@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,1815 @@
1
+ // src/native/components/CheckoutButton.tsx
2
+ import { useState } from "react";
3
+ import {
4
+ TouchableOpacity,
5
+ Text,
6
+ ActivityIndicator,
7
+ StyleSheet,
8
+ Alert
9
+ } from "react-native";
10
+ import { useStripe } from "@stripe/stripe-react-native";
11
+
12
+ // src/shared/providers/PaymentsProvider.tsx
13
+ import { createContext, useContext } from "react";
14
+ import { jsx } from "react/jsx-runtime";
15
+ var PaymentsContext = createContext(
16
+ void 0
17
+ );
18
+ var usePaymentsConfig = () => {
19
+ const context = useContext(PaymentsContext);
20
+ if (!context) {
21
+ throw new Error("usePaymentsConfig must be used within PaymentsProvider");
22
+ }
23
+ return context;
24
+ };
25
+
26
+ // src/shared/constants.ts
27
+ var CURRENCY_SYMBOLS = {
28
+ USD: "$",
29
+ EUR: "\u20AC",
30
+ GBP: "\xA3",
31
+ JPY: "\xA5",
32
+ CAD: "C$",
33
+ AUD: "A$",
34
+ CHF: "CHF",
35
+ CNY: "\xA5",
36
+ SEK: "kr",
37
+ NZD: "NZ$"
38
+ };
39
+ var DEFAULT_CURRENCY = "USD";
40
+ var ERROR_MESSAGES = {
41
+ PROVIDER_NOT_CONFIGURED: "Payments provider is not properly configured",
42
+ STRIPE_NOT_LOADED: "Stripe has not been loaded yet",
43
+ INVALID_PRICE_ID: "Invalid price ID provided",
44
+ INVALID_CUSTOMER_ID: "Invalid customer ID provided",
45
+ CHECKOUT_FAILED: "Checkout session creation failed",
46
+ PAYMENT_FAILED: "Payment processing failed",
47
+ SUBSCRIPTION_NOT_FOUND: "Subscription not found",
48
+ CUSTOMER_NOT_FOUND: "Customer not found"
49
+ };
50
+
51
+ // src/shared/utils/validation.ts
52
+ var validateEmail = (email) => {
53
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
54
+ return emailRegex.test(email);
55
+ };
56
+ var validateStripeId = (id, type) => {
57
+ const prefixes = {
58
+ customer: "cus_",
59
+ subscription: "sub_",
60
+ price: "price_",
61
+ product: "prod_",
62
+ payment_intent: "pi_"
63
+ };
64
+ return id.startsWith(prefixes[type]);
65
+ };
66
+
67
+ // src/native/components/CheckoutButton.tsx
68
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
69
+ var CheckoutButton = ({
70
+ priceId,
71
+ customerId,
72
+ customerEmail,
73
+ children,
74
+ style,
75
+ textStyle,
76
+ disabled = false,
77
+ metadata,
78
+ trialPeriodDays,
79
+ onSuccess,
80
+ onError,
81
+ onLoading
82
+ }) => {
83
+ const { initPaymentSheet, presentPaymentSheet } = useStripe();
84
+ const { isConfigured } = usePaymentsConfig();
85
+ const [loading, setLoading] = useState(false);
86
+ const handleError = (errorMessage) => {
87
+ onError?.(errorMessage);
88
+ Alert.alert("Payment Error", errorMessage);
89
+ console.error("Checkout error:", errorMessage);
90
+ };
91
+ const handleCheckout = async () => {
92
+ if (!isConfigured) {
93
+ handleError(ERROR_MESSAGES.PROVIDER_NOT_CONFIGURED);
94
+ return;
95
+ }
96
+ if (!validateStripeId(priceId, "price")) {
97
+ handleError(ERROR_MESSAGES.INVALID_PRICE_ID);
98
+ return;
99
+ }
100
+ if (customerId && !validateStripeId(customerId, "customer")) {
101
+ handleError(ERROR_MESSAGES.INVALID_CUSTOMER_ID);
102
+ return;
103
+ }
104
+ setLoading(true);
105
+ onLoading?.(true);
106
+ try {
107
+ const response = await fetch("/api/payments/create-subscription-setup", {
108
+ method: "POST",
109
+ headers: { "Content-Type": "application/json" },
110
+ body: JSON.stringify({
111
+ priceId,
112
+ customerId,
113
+ customerEmail,
114
+ metadata,
115
+ trialPeriodDays
116
+ })
117
+ });
118
+ if (!response.ok) {
119
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
120
+ throw new Error(errorData.error || ERROR_MESSAGES.CHECKOUT_FAILED);
121
+ }
122
+ const {
123
+ setupIntentClientSecret,
124
+ customerId: returnedCustomerId,
125
+ customerEphemeralKeySecret
126
+ } = await response.json();
127
+ if (!setupIntentClientSecret) {
128
+ throw new Error("No setup intent client secret returned from server");
129
+ }
130
+ const { error: initError } = await initPaymentSheet({
131
+ setupIntentClientSecret,
132
+ customerId: returnedCustomerId,
133
+ customerEphemeralKeySecret,
134
+ merchantDisplayName: "Your App Name",
135
+ // TODO: Make this configurable
136
+ allowsDelayedPaymentMethods: true
137
+ });
138
+ if (initError) {
139
+ throw new Error(
140
+ initError.message || "Failed to initialize payment sheet"
141
+ );
142
+ }
143
+ const { error: presentError } = await presentPaymentSheet();
144
+ if (presentError) {
145
+ if (presentError.code !== "Canceled") {
146
+ throw new Error(
147
+ presentError.message || ERROR_MESSAGES.PAYMENT_FAILED
148
+ );
149
+ }
150
+ return;
151
+ }
152
+ onSuccess?.();
153
+ } catch (error) {
154
+ const errorMessage = error instanceof Error ? error.message : ERROR_MESSAGES.CHECKOUT_FAILED;
155
+ handleError(errorMessage);
156
+ } finally {
157
+ setLoading(false);
158
+ onLoading?.(false);
159
+ }
160
+ };
161
+ const isDisabled = loading || disabled || !isConfigured;
162
+ return /* @__PURE__ */ jsxs(
163
+ TouchableOpacity,
164
+ {
165
+ style: [styles.button, style, isDisabled && styles.disabled],
166
+ onPress: handleCheckout,
167
+ disabled: isDisabled,
168
+ activeOpacity: 0.8,
169
+ children: [
170
+ loading ? /* @__PURE__ */ jsx2(ActivityIndicator, { color: "#ffffff", size: "small", style: styles.loader }) : null,
171
+ /* @__PURE__ */ jsx2(
172
+ Text,
173
+ {
174
+ style: [styles.buttonText, textStyle, loading && styles.loadingText],
175
+ children: loading ? "Processing..." : children
176
+ }
177
+ )
178
+ ]
179
+ }
180
+ );
181
+ };
182
+ var styles = StyleSheet.create({
183
+ button: {
184
+ backgroundColor: "#007AFF",
185
+ paddingHorizontal: 16,
186
+ paddingVertical: 12,
187
+ borderRadius: 8,
188
+ flexDirection: "row",
189
+ alignItems: "center",
190
+ justifyContent: "center",
191
+ minHeight: 48
192
+ },
193
+ buttonText: {
194
+ color: "#ffffff",
195
+ fontSize: 16,
196
+ fontWeight: "600",
197
+ textAlign: "center"
198
+ },
199
+ disabled: {
200
+ opacity: 0.5
201
+ },
202
+ loader: {
203
+ marginRight: 8
204
+ },
205
+ loadingText: {
206
+ marginLeft: 8
207
+ }
208
+ });
209
+
210
+ // src/native/components/PricingTable.tsx
211
+ import {
212
+ View,
213
+ Text as Text2,
214
+ ScrollView,
215
+ StyleSheet as StyleSheet2,
216
+ Dimensions
217
+ } from "react-native";
218
+
219
+ // src/shared/utils/formatting.ts
220
+ var formatCurrency = (amount, currency = DEFAULT_CURRENCY, options = {}) => {
221
+ const { showSymbol = true, showCents = true, locale = "en-US" } = options;
222
+ const currencyCode = currency.toUpperCase();
223
+ const isZeroDecimalCurrency = ["JPY", "KRW", "VND", "CLP"].includes(
224
+ currencyCode
225
+ );
226
+ const displayAmount = isZeroDecimalCurrency ? amount : amount / 100;
227
+ if (showSymbol) {
228
+ try {
229
+ return new Intl.NumberFormat(locale, {
230
+ style: "currency",
231
+ currency: currencyCode,
232
+ minimumFractionDigits: showCents && !isZeroDecimalCurrency ? 2 : 0,
233
+ maximumFractionDigits: showCents && !isZeroDecimalCurrency ? 2 : 0
234
+ }).format(displayAmount);
235
+ } catch (error) {
236
+ const symbol = CURRENCY_SYMBOLS[currencyCode] || currencyCode;
237
+ const formattedAmount = showCents && !isZeroDecimalCurrency ? displayAmount.toFixed(2) : Math.round(displayAmount).toString();
238
+ return `${symbol}${formattedAmount}`;
239
+ }
240
+ }
241
+ return showCents && !isZeroDecimalCurrency ? displayAmount.toFixed(2) : Math.round(displayAmount).toString();
242
+ };
243
+ var formatPricingPlan = (plan) => {
244
+ const price = formatCurrency(plan.price, plan.currency);
245
+ const interval = plan.intervalCount && plan.intervalCount > 1 ? `${plan.intervalCount} ${plan.interval}s` : plan.interval;
246
+ return `${price}/${interval}`;
247
+ };
248
+ var formatTrialPeriod = (days) => {
249
+ if (days === 0) return "No trial";
250
+ if (days === 1) return "1 day trial";
251
+ if (days < 7) return `${days} days trial`;
252
+ if (days === 7) return "1 week trial";
253
+ if (days < 30) return `${Math.floor(days / 7)} weeks trial`;
254
+ if (days === 30) return "1 month trial";
255
+ return `${Math.floor(days / 30)} months trial`;
256
+ };
257
+
258
+ // src/native/components/PricingTable.tsx
259
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
260
+ var { width: screenWidth } = Dimensions.get("window");
261
+ var PricingTable = ({
262
+ plans,
263
+ customerId,
264
+ customerEmail,
265
+ onPlanSelect,
266
+ onCheckoutSuccess,
267
+ onCheckoutError,
268
+ showFeatures = true,
269
+ showTrialInfo = true,
270
+ layout = "carousel",
271
+ containerStyle
272
+ }) => {
273
+ if (!plans || plans.length === 0) {
274
+ return /* @__PURE__ */ jsx3(View, { style: styles2.emptyContainer, children: /* @__PURE__ */ jsx3(Text2, { style: styles2.emptyText, children: "No pricing plans available" }) });
275
+ }
276
+ const renderPricingCard = (plan, index) => /* @__PURE__ */ jsx3(
277
+ PricingCard,
278
+ {
279
+ plan,
280
+ customerId,
281
+ customerEmail,
282
+ onSelect: () => onPlanSelect?.(plan),
283
+ onCheckoutSuccess: () => onCheckoutSuccess?.(plan),
284
+ onCheckoutError: (error) => onCheckoutError?.(plan, error),
285
+ showFeatures,
286
+ showTrialInfo,
287
+ style: layout === "carousel" ? styles2.carouselCard : styles2.listCard
288
+ },
289
+ plan.id
290
+ );
291
+ if (layout === "carousel") {
292
+ return /* @__PURE__ */ jsx3(View, { style: [styles2.container, containerStyle], children: /* @__PURE__ */ jsx3(
293
+ ScrollView,
294
+ {
295
+ horizontal: true,
296
+ showsHorizontalScrollIndicator: false,
297
+ contentContainerStyle: styles2.carouselContainer,
298
+ snapToInterval: screenWidth * 0.8 + 16,
299
+ decelerationRate: "fast",
300
+ children: plans.map(renderPricingCard)
301
+ }
302
+ ) });
303
+ }
304
+ return /* @__PURE__ */ jsx3(ScrollView, { style: [styles2.container, containerStyle], children: /* @__PURE__ */ jsx3(View, { style: styles2.listContainer, children: plans.map(renderPricingCard) }) });
305
+ };
306
+ var PricingCard = ({
307
+ plan,
308
+ customerId,
309
+ customerEmail,
310
+ onSelect,
311
+ onCheckoutSuccess,
312
+ onCheckoutError,
313
+ showFeatures = true,
314
+ showTrialInfo = true,
315
+ style
316
+ }) => {
317
+ const isPopular = plan.popular;
318
+ return /* @__PURE__ */ jsxs2(View, { style: [styles2.card, isPopular && styles2.popularCard, style], children: [
319
+ isPopular && /* @__PURE__ */ jsx3(View, { style: styles2.popularBadge, children: /* @__PURE__ */ jsx3(Text2, { style: styles2.popularBadgeText, children: "Most Popular" }) }),
320
+ /* @__PURE__ */ jsxs2(View, { style: styles2.cardHeader, children: [
321
+ /* @__PURE__ */ jsx3(Text2, { style: styles2.planName, children: plan.name }),
322
+ plan.description && /* @__PURE__ */ jsx3(Text2, { style: styles2.planDescription, children: plan.description }),
323
+ /* @__PURE__ */ jsx3(Text2, { style: styles2.planPrice, children: formatPricingPlan(plan) }),
324
+ showTrialInfo && plan.trialPeriodDays && plan.trialPeriodDays > 0 && /* @__PURE__ */ jsx3(Text2, { style: styles2.trialInfo, children: formatTrialPeriod(plan.trialPeriodDays) })
325
+ ] }),
326
+ showFeatures && plan.features && plan.features.length > 0 && /* @__PURE__ */ jsx3(View, { style: styles2.featuresContainer, children: plan.features.map((feature, index) => /* @__PURE__ */ jsxs2(View, { style: styles2.featureItem, children: [
327
+ /* @__PURE__ */ jsx3(Text2, { style: styles2.featureCheckmark, children: "\u2713" }),
328
+ /* @__PURE__ */ jsx3(Text2, { style: styles2.featureText, children: feature })
329
+ ] }, index)) }),
330
+ /* @__PURE__ */ jsx3(View, { style: styles2.buttonContainer, children: /* @__PURE__ */ jsx3(
331
+ CheckoutButton,
332
+ {
333
+ priceId: plan.stripePriceId,
334
+ customerId,
335
+ customerEmail,
336
+ trialPeriodDays: plan.trialPeriodDays,
337
+ metadata: plan.metadata,
338
+ onSuccess: onCheckoutSuccess,
339
+ onError: onCheckoutError,
340
+ style: [
341
+ styles2.ctaButton,
342
+ isPopular ? styles2.popularButton : styles2.regularButton
343
+ ],
344
+ textStyle: [
345
+ styles2.ctaButtonText,
346
+ isPopular ? styles2.popularButtonText : styles2.regularButtonText
347
+ ],
348
+ children: "Get Started"
349
+ }
350
+ ) })
351
+ ] });
352
+ };
353
+ var styles2 = StyleSheet2.create({
354
+ container: {
355
+ flex: 1
356
+ },
357
+ emptyContainer: {
358
+ flex: 1,
359
+ justifyContent: "center",
360
+ alignItems: "center",
361
+ padding: 20
362
+ },
363
+ emptyText: {
364
+ fontSize: 16,
365
+ color: "#666",
366
+ textAlign: "center"
367
+ },
368
+ carouselContainer: {
369
+ paddingHorizontal: 16
370
+ },
371
+ listContainer: {
372
+ padding: 16
373
+ },
374
+ carouselCard: {
375
+ width: screenWidth * 0.8,
376
+ marginRight: 16
377
+ },
378
+ listCard: {
379
+ marginBottom: 16
380
+ },
381
+ card: {
382
+ backgroundColor: "#ffffff",
383
+ borderRadius: 12,
384
+ padding: 20,
385
+ shadowColor: "#000",
386
+ shadowOffset: {
387
+ width: 0,
388
+ height: 2
389
+ },
390
+ shadowOpacity: 0.1,
391
+ shadowRadius: 8,
392
+ elevation: 4,
393
+ borderWidth: 1,
394
+ borderColor: "#e0e0e0"
395
+ },
396
+ popularCard: {
397
+ borderColor: "#007AFF",
398
+ borderWidth: 2
399
+ },
400
+ popularBadge: {
401
+ position: "absolute",
402
+ top: -1,
403
+ left: -1,
404
+ right: -1,
405
+ backgroundColor: "#007AFF",
406
+ paddingVertical: 8,
407
+ borderTopLeftRadius: 12,
408
+ borderTopRightRadius: 12,
409
+ alignItems: "center"
410
+ },
411
+ popularBadgeText: {
412
+ color: "#ffffff",
413
+ fontSize: 12,
414
+ fontWeight: "600"
415
+ },
416
+ cardHeader: {
417
+ alignItems: "center",
418
+ marginBottom: 20,
419
+ marginTop: 20
420
+ // Extra space for popular badge
421
+ },
422
+ planName: {
423
+ fontSize: 20,
424
+ fontWeight: "700",
425
+ color: "#333",
426
+ marginBottom: 8,
427
+ textAlign: "center"
428
+ },
429
+ planDescription: {
430
+ fontSize: 14,
431
+ color: "#666",
432
+ textAlign: "center",
433
+ marginBottom: 16,
434
+ lineHeight: 20
435
+ },
436
+ planPrice: {
437
+ fontSize: 28,
438
+ fontWeight: "800",
439
+ color: "#333",
440
+ marginBottom: 8,
441
+ textAlign: "center"
442
+ },
443
+ trialInfo: {
444
+ fontSize: 12,
445
+ color: "#22c55e",
446
+ fontWeight: "600",
447
+ textAlign: "center"
448
+ },
449
+ featuresContainer: {
450
+ marginBottom: 24
451
+ },
452
+ featureItem: {
453
+ flexDirection: "row",
454
+ alignItems: "flex-start",
455
+ marginBottom: 12
456
+ },
457
+ featureCheckmark: {
458
+ color: "#22c55e",
459
+ fontSize: 16,
460
+ fontWeight: "700",
461
+ marginRight: 12,
462
+ marginTop: 2
463
+ },
464
+ featureText: {
465
+ flex: 1,
466
+ fontSize: 14,
467
+ color: "#555",
468
+ lineHeight: 20
469
+ },
470
+ buttonContainer: {
471
+ marginTop: "auto"
472
+ },
473
+ ctaButton: {
474
+ paddingVertical: 16,
475
+ borderRadius: 8
476
+ },
477
+ ctaButtonText: {
478
+ fontSize: 16,
479
+ fontWeight: "600"
480
+ },
481
+ popularButton: {
482
+ backgroundColor: "#007AFF"
483
+ },
484
+ popularButtonText: {
485
+ color: "#ffffff"
486
+ },
487
+ regularButton: {
488
+ backgroundColor: "#f8f9fa",
489
+ borderWidth: 1,
490
+ borderColor: "#e0e0e0"
491
+ },
492
+ regularButtonText: {
493
+ color: "#333"
494
+ }
495
+ });
496
+
497
+ // src/native/components/PaymentForm.tsx
498
+ import { useState as useState2 } from "react";
499
+ import {
500
+ View as View2,
501
+ Text as Text3,
502
+ TextInput,
503
+ TouchableOpacity as TouchableOpacity2,
504
+ StyleSheet as StyleSheet3,
505
+ Alert as Alert2,
506
+ ActivityIndicator as ActivityIndicator2,
507
+ KeyboardAvoidingView,
508
+ Platform,
509
+ ScrollView as ScrollView2
510
+ } from "react-native";
511
+ import { useStripe as useStripe2, CardField } from "@stripe/stripe-react-native";
512
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
513
+ var PaymentForm = ({
514
+ clientSecret,
515
+ customerId,
516
+ customerEmail,
517
+ amount,
518
+ currency = "usd",
519
+ description,
520
+ metadata,
521
+ onSuccess,
522
+ onError,
523
+ onLoading,
524
+ containerStyle,
525
+ showBillingDetails = true
526
+ }) => {
527
+ const { confirmPayment } = useStripe2();
528
+ const { isConfigured } = usePaymentsConfig();
529
+ const [loading, setLoading] = useState2(false);
530
+ const [email, setEmail] = useState2(customerEmail || "");
531
+ const [name, setName] = useState2("");
532
+ const [phone, setPhone] = useState2("");
533
+ const [cardComplete, setCardComplete] = useState2(false);
534
+ const handleError = (errorMessage) => {
535
+ onError?.(errorMessage);
536
+ Alert2.alert("Payment Error", errorMessage);
537
+ console.error("Payment error:", errorMessage);
538
+ };
539
+ const handlePayment = async () => {
540
+ if (!isConfigured) {
541
+ handleError(ERROR_MESSAGES.PROVIDER_NOT_CONFIGURED);
542
+ return;
543
+ }
544
+ if (showBillingDetails && email && !validateEmail(email)) {
545
+ handleError("Please enter a valid email address");
546
+ return;
547
+ }
548
+ if (!cardComplete) {
549
+ handleError("Please complete your card information");
550
+ return;
551
+ }
552
+ let paymentClientSecret = clientSecret;
553
+ if (!paymentClientSecret && amount) {
554
+ try {
555
+ const response = await fetch("/api/payments/create-payment-intent", {
556
+ method: "POST",
557
+ headers: { "Content-Type": "application/json" },
558
+ body: JSON.stringify({
559
+ amount,
560
+ currency,
561
+ customerId,
562
+ description,
563
+ metadata
564
+ })
565
+ });
566
+ if (!response.ok) {
567
+ throw new Error("Failed to create payment intent");
568
+ }
569
+ const { client_secret } = await response.json();
570
+ paymentClientSecret = client_secret;
571
+ } catch (error) {
572
+ handleError("Failed to initialize payment");
573
+ return;
574
+ }
575
+ }
576
+ if (!paymentClientSecret) {
577
+ handleError("Payment could not be initialized");
578
+ return;
579
+ }
580
+ setLoading(true);
581
+ onLoading?.(true);
582
+ try {
583
+ const billingDetails = showBillingDetails ? {
584
+ email: email || void 0,
585
+ name: name || void 0,
586
+ phone: phone || void 0
587
+ } : void 0;
588
+ const { error, paymentIntent } = await confirmPayment(
589
+ paymentClientSecret,
590
+ {
591
+ paymentMethodType: "Card",
592
+ paymentMethodData: {
593
+ billingDetails
594
+ }
595
+ }
596
+ );
597
+ if (error) {
598
+ throw new Error(error.message || ERROR_MESSAGES.PAYMENT_FAILED);
599
+ }
600
+ if (paymentIntent?.status === "Succeeded") {
601
+ onSuccess?.(paymentIntent);
602
+ Alert2.alert("Success", "Payment completed successfully!");
603
+ } else {
604
+ throw new Error("Payment was not completed");
605
+ }
606
+ } catch (error) {
607
+ const errorMessage = error instanceof Error ? error.message : ERROR_MESSAGES.PAYMENT_FAILED;
608
+ handleError(errorMessage);
609
+ } finally {
610
+ setLoading(false);
611
+ onLoading?.(false);
612
+ }
613
+ };
614
+ return /* @__PURE__ */ jsx4(
615
+ KeyboardAvoidingView,
616
+ {
617
+ behavior: Platform.OS === "ios" ? "padding" : "height",
618
+ style: [styles3.container, containerStyle],
619
+ children: /* @__PURE__ */ jsxs3(ScrollView2, { showsVerticalScrollIndicator: false, children: [
620
+ showBillingDetails && /* @__PURE__ */ jsxs3(View2, { style: styles3.billingSection, children: [
621
+ /* @__PURE__ */ jsx4(Text3, { style: styles3.sectionTitle, children: "Billing Information" }),
622
+ /* @__PURE__ */ jsxs3(View2, { style: styles3.inputContainer, children: [
623
+ /* @__PURE__ */ jsx4(Text3, { style: styles3.inputLabel, children: "Email *" }),
624
+ /* @__PURE__ */ jsx4(
625
+ TextInput,
626
+ {
627
+ style: styles3.textInput,
628
+ value: email,
629
+ onChangeText: setEmail,
630
+ placeholder: "your@email.com",
631
+ keyboardType: "email-address",
632
+ autoCapitalize: "none",
633
+ autoCorrect: false
634
+ }
635
+ )
636
+ ] }),
637
+ /* @__PURE__ */ jsxs3(View2, { style: styles3.inputContainer, children: [
638
+ /* @__PURE__ */ jsx4(Text3, { style: styles3.inputLabel, children: "Full Name" }),
639
+ /* @__PURE__ */ jsx4(
640
+ TextInput,
641
+ {
642
+ style: styles3.textInput,
643
+ value: name,
644
+ onChangeText: setName,
645
+ placeholder: "John Doe",
646
+ autoCapitalize: "words"
647
+ }
648
+ )
649
+ ] }),
650
+ /* @__PURE__ */ jsxs3(View2, { style: styles3.inputContainer, children: [
651
+ /* @__PURE__ */ jsx4(Text3, { style: styles3.inputLabel, children: "Phone" }),
652
+ /* @__PURE__ */ jsx4(
653
+ TextInput,
654
+ {
655
+ style: styles3.textInput,
656
+ value: phone,
657
+ onChangeText: setPhone,
658
+ placeholder: "+1 (555) 123-4567",
659
+ keyboardType: "phone-pad"
660
+ }
661
+ )
662
+ ] })
663
+ ] }),
664
+ /* @__PURE__ */ jsxs3(View2, { style: styles3.cardSection, children: [
665
+ /* @__PURE__ */ jsx4(Text3, { style: styles3.sectionTitle, children: "Payment Information" }),
666
+ /* @__PURE__ */ jsx4(View2, { style: styles3.cardContainer, children: /* @__PURE__ */ jsx4(
667
+ CardField,
668
+ {
669
+ postalCodeEnabled: true,
670
+ placeholders: {
671
+ number: "4242 4242 4242 4242"
672
+ },
673
+ cardStyle: styles3.cardField,
674
+ style: styles3.cardFieldContainer,
675
+ onCardChange: (cardDetails) => {
676
+ setCardComplete(cardDetails.complete);
677
+ }
678
+ }
679
+ ) })
680
+ ] }),
681
+ /* @__PURE__ */ jsx4(
682
+ TouchableOpacity2,
683
+ {
684
+ style: [styles3.payButton, loading && styles3.payButtonDisabled],
685
+ onPress: handlePayment,
686
+ disabled: loading || !cardComplete,
687
+ activeOpacity: 0.8,
688
+ children: loading ? /* @__PURE__ */ jsx4(ActivityIndicator2, { color: "#ffffff", size: "small" }) : /* @__PURE__ */ jsxs3(Text3, { style: styles3.payButtonText, children: [
689
+ "Pay ",
690
+ amount ? `$${(amount / 100).toFixed(2)}` : ""
691
+ ] })
692
+ }
693
+ )
694
+ ] })
695
+ }
696
+ );
697
+ };
698
+ var styles3 = StyleSheet3.create({
699
+ container: {
700
+ flex: 1,
701
+ padding: 20,
702
+ backgroundColor: "#ffffff"
703
+ },
704
+ billingSection: {
705
+ marginBottom: 24
706
+ },
707
+ cardSection: {
708
+ marginBottom: 32
709
+ },
710
+ sectionTitle: {
711
+ fontSize: 18,
712
+ fontWeight: "600",
713
+ color: "#333",
714
+ marginBottom: 16
715
+ },
716
+ inputContainer: {
717
+ marginBottom: 16
718
+ },
719
+ inputLabel: {
720
+ fontSize: 14,
721
+ fontWeight: "500",
722
+ color: "#555",
723
+ marginBottom: 8
724
+ },
725
+ textInput: {
726
+ borderWidth: 1,
727
+ borderColor: "#ddd",
728
+ borderRadius: 8,
729
+ paddingHorizontal: 12,
730
+ paddingVertical: 12,
731
+ fontSize: 16,
732
+ backgroundColor: "#fafafa"
733
+ },
734
+ cardContainer: {
735
+ borderWidth: 1,
736
+ borderColor: "#ddd",
737
+ borderRadius: 8,
738
+ backgroundColor: "#fafafa"
739
+ },
740
+ cardFieldContainer: {
741
+ width: "100%",
742
+ height: 50
743
+ },
744
+ cardField: {
745
+ backgroundColor: "#fafafa",
746
+ textColor: "#333",
747
+ fontSize: 16,
748
+ placeholderColor: "#999"
749
+ },
750
+ payButton: {
751
+ backgroundColor: "#007AFF",
752
+ paddingVertical: 16,
753
+ borderRadius: 8,
754
+ alignItems: "center",
755
+ justifyContent: "center",
756
+ minHeight: 52
757
+ },
758
+ payButtonDisabled: {
759
+ opacity: 0.5
760
+ },
761
+ payButtonText: {
762
+ color: "#ffffff",
763
+ fontSize: 18,
764
+ fontWeight: "600"
765
+ }
766
+ });
767
+
768
+ // src/native/providers/StripeProvider.tsx
769
+ import { StripeProvider as RNStripeProvider } from "@stripe/stripe-react-native";
770
+ import { View as View3, Text as Text4, StyleSheet as StyleSheet4 } from "react-native";
771
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
772
+ var StripeProvider = ({
773
+ children,
774
+ merchantIdentifier,
775
+ urlScheme,
776
+ setReturnUrlSchemeOnAndroid = false
777
+ }) => {
778
+ const { config, isConfigured } = usePaymentsConfig();
779
+ if (!isConfigured) {
780
+ if (config.environment === "development") {
781
+ return /* @__PURE__ */ jsxs4(View3, { style: styles4.errorContainer, children: [
782
+ /* @__PURE__ */ jsx5(Text4, { style: styles4.errorTitle, children: "Payments Provider Error" }),
783
+ /* @__PURE__ */ jsx5(Text4, { style: styles4.errorMessage, children: ERROR_MESSAGES.PROVIDER_NOT_CONFIGURED }),
784
+ /* @__PURE__ */ jsx5(Text4, { style: styles4.errorSubtext, children: "Please check your PaymentsProvider configuration." })
785
+ ] });
786
+ }
787
+ return null;
788
+ }
789
+ return /* @__PURE__ */ jsx5(
790
+ RNStripeProvider,
791
+ {
792
+ publishableKey: config.publishableKey,
793
+ merchantIdentifier,
794
+ urlScheme,
795
+ setReturnUrlSchemeOnAndroid,
796
+ children
797
+ }
798
+ );
799
+ };
800
+ var styles4 = StyleSheet4.create({
801
+ errorContainer: {
802
+ padding: 20,
803
+ backgroundColor: "#fee",
804
+ borderColor: "#fcc",
805
+ borderWidth: 1,
806
+ borderRadius: 4,
807
+ margin: 10
808
+ },
809
+ errorTitle: {
810
+ fontWeight: "bold",
811
+ fontSize: 16,
812
+ color: "#c53030",
813
+ marginBottom: 8
814
+ },
815
+ errorMessage: {
816
+ fontSize: 14,
817
+ color: "#c53030",
818
+ marginBottom: 4
819
+ },
820
+ errorSubtext: {
821
+ fontSize: 12,
822
+ color: "#a0a0a0"
823
+ }
824
+ });
825
+
826
+ // src/native/hooks/useSubscription.ts
827
+ import { useState as useState3, useEffect, useCallback } from "react";
828
+ import { Alert as Alert3 } from "react-native";
829
+ var useSubscription = (options = {}) => {
830
+ const {
831
+ customerId,
832
+ subscriptionId,
833
+ autoFetch = true,
834
+ onError,
835
+ showErrorAlert = true
836
+ } = options;
837
+ const [subscription, setSubscription] = useState3(null);
838
+ const [subscriptions, setSubscriptions] = useState3([]);
839
+ const [loading, setLoading] = useState3(false);
840
+ const [error, setError] = useState3(null);
841
+ const handleError = useCallback(
842
+ (errorMessage) => {
843
+ setError(errorMessage);
844
+ onError?.(errorMessage);
845
+ if (showErrorAlert) {
846
+ Alert3.alert("Subscription Error", errorMessage);
847
+ }
848
+ console.error("Subscription error:", errorMessage);
849
+ },
850
+ [onError, showErrorAlert]
851
+ );
852
+ const fetchSubscription2 = useCallback(
853
+ async (id) => {
854
+ if (!validateStripeId(id, "subscription")) {
855
+ handleError("Invalid subscription ID");
856
+ return null;
857
+ }
858
+ try {
859
+ const response = await fetch(`/api/payments/subscription/${id}`);
860
+ if (!response.ok) {
861
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
862
+ throw new Error(
863
+ errorData.error || ERROR_MESSAGES.SUBSCRIPTION_NOT_FOUND
864
+ );
865
+ }
866
+ const data = await response.json();
867
+ return data;
868
+ } catch (err) {
869
+ const errorMessage = err instanceof Error ? err.message : ERROR_MESSAGES.SUBSCRIPTION_NOT_FOUND;
870
+ handleError(errorMessage);
871
+ return null;
872
+ }
873
+ },
874
+ [handleError]
875
+ );
876
+ const fetchCustomerSubscriptions2 = useCallback(
877
+ async (id) => {
878
+ if (!validateStripeId(id, "customer")) {
879
+ handleError("Invalid customer ID");
880
+ return [];
881
+ }
882
+ try {
883
+ const response = await fetch(
884
+ `/api/payments/customer/${id}/subscriptions`
885
+ );
886
+ if (!response.ok) {
887
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
888
+ throw new Error(errorData.error || ERROR_MESSAGES.CUSTOMER_NOT_FOUND);
889
+ }
890
+ const data = await response.json();
891
+ return data.subscriptions || [];
892
+ } catch (err) {
893
+ const errorMessage = err instanceof Error ? err.message : ERROR_MESSAGES.CUSTOMER_NOT_FOUND;
894
+ handleError(errorMessage);
895
+ return [];
896
+ }
897
+ },
898
+ [handleError]
899
+ );
900
+ const refetch = useCallback(async () => {
901
+ if (!customerId && !subscriptionId) {
902
+ handleError("Either customerId or subscriptionId is required");
903
+ return;
904
+ }
905
+ setLoading(true);
906
+ setError(null);
907
+ try {
908
+ if (subscriptionId) {
909
+ const sub = await fetchSubscription2(subscriptionId);
910
+ setSubscription(sub);
911
+ }
912
+ if (customerId) {
913
+ const subs = await fetchCustomerSubscriptions2(customerId);
914
+ setSubscriptions(subs);
915
+ if (!subscriptionId && subs.length > 0) {
916
+ const activeSub = subs.find((s) => s.status === "active") || subs[0];
917
+ setSubscription(activeSub);
918
+ }
919
+ }
920
+ } finally {
921
+ setLoading(false);
922
+ }
923
+ }, [
924
+ customerId,
925
+ subscriptionId,
926
+ fetchSubscription2,
927
+ fetchCustomerSubscriptions2,
928
+ handleError
929
+ ]);
930
+ const cancel = useCallback(
931
+ async (targetSubscriptionId) => {
932
+ const idToCancel = targetSubscriptionId || subscriptionId;
933
+ if (!idToCancel) {
934
+ handleError("Subscription ID is required to cancel");
935
+ return false;
936
+ }
937
+ if (!validateStripeId(idToCancel, "subscription")) {
938
+ handleError("Invalid subscription ID");
939
+ return false;
940
+ }
941
+ return new Promise((resolve) => {
942
+ Alert3.alert(
943
+ "Cancel Subscription",
944
+ "Are you sure you want to cancel your subscription? It will remain active until the end of your current billing period.",
945
+ [
946
+ {
947
+ text: "Keep Subscription",
948
+ style: "cancel",
949
+ onPress: () => resolve(false)
950
+ },
951
+ {
952
+ text: "Cancel Subscription",
953
+ style: "destructive",
954
+ onPress: async () => {
955
+ try {
956
+ const response = await fetch(
957
+ `/api/payments/subscription/${idToCancel}/cancel`,
958
+ {
959
+ method: "POST",
960
+ headers: { "Content-Type": "application/json" }
961
+ }
962
+ );
963
+ if (!response.ok) {
964
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
965
+ throw new Error(
966
+ errorData.error || "Failed to cancel subscription"
967
+ );
968
+ }
969
+ await refetch();
970
+ Alert3.alert(
971
+ "Success",
972
+ "Your subscription has been cancelled and will end at the end of your current billing period."
973
+ );
974
+ resolve(true);
975
+ } catch (err) {
976
+ const errorMessage = err instanceof Error ? err.message : "Failed to cancel subscription";
977
+ handleError(errorMessage);
978
+ resolve(false);
979
+ }
980
+ }
981
+ }
982
+ ]
983
+ );
984
+ });
985
+ },
986
+ [subscriptionId, refetch, handleError]
987
+ );
988
+ const reactivate = useCallback(
989
+ async (targetSubscriptionId) => {
990
+ const idToReactivate = targetSubscriptionId || subscriptionId;
991
+ if (!idToReactivate) {
992
+ handleError("Subscription ID is required to reactivate");
993
+ return false;
994
+ }
995
+ if (!validateStripeId(idToReactivate, "subscription")) {
996
+ handleError("Invalid subscription ID");
997
+ return false;
998
+ }
999
+ try {
1000
+ const response = await fetch(
1001
+ `/api/payments/subscription/${idToReactivate}/reactivate`,
1002
+ {
1003
+ method: "POST",
1004
+ headers: { "Content-Type": "application/json" }
1005
+ }
1006
+ );
1007
+ if (!response.ok) {
1008
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1009
+ throw new Error(
1010
+ errorData.error || "Failed to reactivate subscription"
1011
+ );
1012
+ }
1013
+ await refetch();
1014
+ Alert3.alert("Success", "Your subscription has been reactivated!");
1015
+ return true;
1016
+ } catch (err) {
1017
+ const errorMessage = err instanceof Error ? err.message : "Failed to reactivate subscription";
1018
+ handleError(errorMessage);
1019
+ return false;
1020
+ }
1021
+ },
1022
+ [subscriptionId, refetch, handleError]
1023
+ );
1024
+ const updatePaymentMethod = useCallback(
1025
+ async (targetSubscriptionId, paymentMethodId) => {
1026
+ if (!validateStripeId(targetSubscriptionId, "subscription")) {
1027
+ handleError("Invalid subscription ID");
1028
+ return false;
1029
+ }
1030
+ try {
1031
+ const response = await fetch(
1032
+ `/api/payments/subscription/${targetSubscriptionId}/payment-method`,
1033
+ {
1034
+ method: "PUT",
1035
+ headers: { "Content-Type": "application/json" },
1036
+ body: JSON.stringify({ paymentMethodId })
1037
+ }
1038
+ );
1039
+ if (!response.ok) {
1040
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1041
+ throw new Error(errorData.error || "Failed to update payment method");
1042
+ }
1043
+ await refetch();
1044
+ Alert3.alert("Success", "Payment method updated successfully!");
1045
+ return true;
1046
+ } catch (err) {
1047
+ const errorMessage = err instanceof Error ? err.message : "Failed to update payment method";
1048
+ handleError(errorMessage);
1049
+ return false;
1050
+ }
1051
+ },
1052
+ [refetch, handleError]
1053
+ );
1054
+ useEffect(() => {
1055
+ if (autoFetch && (customerId || subscriptionId)) {
1056
+ refetch();
1057
+ }
1058
+ }, [autoFetch, customerId, subscriptionId, refetch]);
1059
+ return {
1060
+ subscription,
1061
+ subscriptions,
1062
+ loading,
1063
+ error,
1064
+ refetch,
1065
+ cancel,
1066
+ reactivate,
1067
+ updatePaymentMethod
1068
+ };
1069
+ };
1070
+
1071
+ // src/native/hooks/useCheckout.ts
1072
+ import { useState as useState4, useCallback as useCallback2 } from "react";
1073
+ import { Alert as Alert4 } from "react-native";
1074
+ import { useStripe as useStripe3 } from "@stripe/stripe-react-native";
1075
+ var useCheckout = (options = {}) => {
1076
+ const {
1077
+ onSuccess,
1078
+ onError,
1079
+ onLoading,
1080
+ showErrorAlert = true,
1081
+ showSuccessAlert = true
1082
+ } = options;
1083
+ const { initPaymentSheet, presentPaymentSheet, confirmPayment } = useStripe3();
1084
+ const { isConfigured } = usePaymentsConfig();
1085
+ const [loading, setLoading] = useState4(false);
1086
+ const [error, setError] = useState4(null);
1087
+ const handleError = useCallback2(
1088
+ (errorMessage) => {
1089
+ setError(errorMessage);
1090
+ onError?.(errorMessage);
1091
+ if (showErrorAlert) {
1092
+ Alert4.alert("Payment Error", errorMessage);
1093
+ }
1094
+ console.error("Checkout error:", errorMessage);
1095
+ },
1096
+ [onError, showErrorAlert]
1097
+ );
1098
+ const handleSuccess = useCallback2(
1099
+ (result) => {
1100
+ onSuccess?.(result);
1101
+ if (showSuccessAlert) {
1102
+ Alert4.alert("Success", "Payment completed successfully!");
1103
+ }
1104
+ },
1105
+ [onSuccess, showSuccessAlert]
1106
+ );
1107
+ const createSubscription = useCallback2(
1108
+ async (params) => {
1109
+ if (!isConfigured) {
1110
+ handleError(ERROR_MESSAGES.PROVIDER_NOT_CONFIGURED);
1111
+ return false;
1112
+ }
1113
+ if (!validateStripeId(params.priceId, "price")) {
1114
+ handleError(ERROR_MESSAGES.INVALID_PRICE_ID);
1115
+ return false;
1116
+ }
1117
+ if (params.customerId && !validateStripeId(params.customerId, "customer")) {
1118
+ handleError(ERROR_MESSAGES.INVALID_CUSTOMER_ID);
1119
+ return false;
1120
+ }
1121
+ if (params.customerEmail && !validateEmail(params.customerEmail)) {
1122
+ handleError("Invalid customer email");
1123
+ return false;
1124
+ }
1125
+ setLoading(true);
1126
+ setError(null);
1127
+ onLoading?.(true);
1128
+ try {
1129
+ const response = await fetch(
1130
+ "/api/payments/create-subscription-setup",
1131
+ {
1132
+ method: "POST",
1133
+ headers: { "Content-Type": "application/json" },
1134
+ body: JSON.stringify(params)
1135
+ }
1136
+ );
1137
+ if (!response.ok) {
1138
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1139
+ throw new Error(
1140
+ errorData.error || "Failed to create subscription setup"
1141
+ );
1142
+ }
1143
+ const {
1144
+ setupIntentClientSecret,
1145
+ customerId: returnedCustomerId,
1146
+ customerEphemeralKeySecret
1147
+ } = await response.json();
1148
+ if (!setupIntentClientSecret) {
1149
+ throw new Error("No setup intent client secret returned from server");
1150
+ }
1151
+ const { error: initError } = await initPaymentSheet({
1152
+ setupIntentClientSecret,
1153
+ customerId: returnedCustomerId,
1154
+ customerEphemeralKeySecret,
1155
+ merchantDisplayName: "Your App Name",
1156
+ // TODO: Make this configurable
1157
+ allowsDelayedPaymentMethods: true
1158
+ });
1159
+ if (initError) {
1160
+ throw new Error(
1161
+ initError.message || "Failed to initialize payment sheet"
1162
+ );
1163
+ }
1164
+ const { error: presentError } = await presentPaymentSheet();
1165
+ if (presentError) {
1166
+ if (presentError.code === "Canceled") {
1167
+ return false;
1168
+ }
1169
+ throw new Error(
1170
+ presentError.message || ERROR_MESSAGES.PAYMENT_FAILED
1171
+ );
1172
+ }
1173
+ handleSuccess({ type: "subscription", customerId: returnedCustomerId });
1174
+ return true;
1175
+ } catch (err) {
1176
+ const errorMessage = err instanceof Error ? err.message : "Failed to create subscription";
1177
+ handleError(errorMessage);
1178
+ return false;
1179
+ } finally {
1180
+ setLoading(false);
1181
+ onLoading?.(false);
1182
+ }
1183
+ },
1184
+ [
1185
+ isConfigured,
1186
+ initPaymentSheet,
1187
+ presentPaymentSheet,
1188
+ handleError,
1189
+ handleSuccess,
1190
+ onLoading
1191
+ ]
1192
+ );
1193
+ const createPaymentIntent2 = useCallback2(
1194
+ async (params) => {
1195
+ if (!isConfigured) {
1196
+ handleError(ERROR_MESSAGES.PROVIDER_NOT_CONFIGURED);
1197
+ return null;
1198
+ }
1199
+ if (params.amount <= 0) {
1200
+ handleError("Amount must be greater than 0");
1201
+ return null;
1202
+ }
1203
+ if (params.customerId && !validateStripeId(params.customerId, "customer")) {
1204
+ handleError(ERROR_MESSAGES.INVALID_CUSTOMER_ID);
1205
+ return null;
1206
+ }
1207
+ setLoading(true);
1208
+ setError(null);
1209
+ onLoading?.(true);
1210
+ try {
1211
+ const response = await fetch("/api/payments/create-payment-intent", {
1212
+ method: "POST",
1213
+ headers: { "Content-Type": "application/json" },
1214
+ body: JSON.stringify(params)
1215
+ });
1216
+ if (!response.ok) {
1217
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1218
+ throw new Error(errorData.error || "Failed to create payment intent");
1219
+ }
1220
+ const { client_secret } = await response.json();
1221
+ if (!client_secret) {
1222
+ throw new Error("No client secret returned from server");
1223
+ }
1224
+ return client_secret;
1225
+ } catch (err) {
1226
+ const errorMessage = err instanceof Error ? err.message : "Failed to create payment intent";
1227
+ handleError(errorMessage);
1228
+ return null;
1229
+ } finally {
1230
+ setLoading(false);
1231
+ onLoading?.(false);
1232
+ }
1233
+ },
1234
+ [isConfigured, handleError, onLoading]
1235
+ );
1236
+ const processPayment = useCallback2(
1237
+ async (clientSecret, billingDetails) => {
1238
+ if (!isConfigured) {
1239
+ handleError(ERROR_MESSAGES.PROVIDER_NOT_CONFIGURED);
1240
+ return false;
1241
+ }
1242
+ if (billingDetails?.email && !validateEmail(billingDetails.email)) {
1243
+ handleError("Invalid email address");
1244
+ return false;
1245
+ }
1246
+ setLoading(true);
1247
+ setError(null);
1248
+ onLoading?.(true);
1249
+ try {
1250
+ const { error: error2, paymentIntent } = await confirmPayment(clientSecret, {
1251
+ paymentMethodType: "Card",
1252
+ paymentMethodData: {
1253
+ billingDetails
1254
+ }
1255
+ });
1256
+ if (error2) {
1257
+ throw new Error(error2.message || ERROR_MESSAGES.PAYMENT_FAILED);
1258
+ }
1259
+ if (paymentIntent?.status === "Succeeded") {
1260
+ handleSuccess({ type: "payment", paymentIntent });
1261
+ return true;
1262
+ } else {
1263
+ throw new Error("Payment was not completed");
1264
+ }
1265
+ } catch (err) {
1266
+ const errorMessage = err instanceof Error ? err.message : ERROR_MESSAGES.PAYMENT_FAILED;
1267
+ handleError(errorMessage);
1268
+ return false;
1269
+ } finally {
1270
+ setLoading(false);
1271
+ onLoading?.(false);
1272
+ }
1273
+ },
1274
+ [isConfigured, confirmPayment, handleError, handleSuccess, onLoading]
1275
+ );
1276
+ return {
1277
+ loading,
1278
+ error,
1279
+ createSubscription,
1280
+ createPaymentIntent: createPaymentIntent2,
1281
+ processPayment
1282
+ };
1283
+ };
1284
+
1285
+ // src/native/hooks/useCustomer.ts
1286
+ import { useState as useState5, useEffect as useEffect2, useCallback as useCallback3 } from "react";
1287
+ import { Alert as Alert5 } from "react-native";
1288
+ var useCustomer = (options = {}) => {
1289
+ const {
1290
+ customerId,
1291
+ autoFetch = true,
1292
+ onError,
1293
+ showErrorAlert = true,
1294
+ showSuccessAlert = true
1295
+ } = options;
1296
+ const [customer, setCustomer] = useState5(null);
1297
+ const [paymentMethods, setPaymentMethods] = useState5([]);
1298
+ const [loading, setLoading] = useState5(false);
1299
+ const [error, setError] = useState5(null);
1300
+ const handleError = useCallback3(
1301
+ (errorMessage) => {
1302
+ setError(errorMessage);
1303
+ onError?.(errorMessage);
1304
+ if (showErrorAlert) {
1305
+ Alert5.alert("Customer Error", errorMessage);
1306
+ }
1307
+ console.error("Customer error:", errorMessage);
1308
+ },
1309
+ [onError, showErrorAlert]
1310
+ );
1311
+ const handleSuccess = useCallback3(
1312
+ (message) => {
1313
+ if (showSuccessAlert) {
1314
+ Alert5.alert("Success", message);
1315
+ }
1316
+ },
1317
+ [showSuccessAlert]
1318
+ );
1319
+ const fetchCustomer = useCallback3(
1320
+ async (id) => {
1321
+ if (!validateStripeId(id, "customer")) {
1322
+ handleError("Invalid customer ID");
1323
+ return null;
1324
+ }
1325
+ try {
1326
+ const response = await fetch(`/api/payments/customer/${id}`);
1327
+ if (!response.ok) {
1328
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1329
+ throw new Error(errorData.error || ERROR_MESSAGES.CUSTOMER_NOT_FOUND);
1330
+ }
1331
+ const data = await response.json();
1332
+ return data;
1333
+ } catch (err) {
1334
+ const errorMessage = err instanceof Error ? err.message : ERROR_MESSAGES.CUSTOMER_NOT_FOUND;
1335
+ handleError(errorMessage);
1336
+ return null;
1337
+ }
1338
+ },
1339
+ [handleError]
1340
+ );
1341
+ const refetch = useCallback3(async () => {
1342
+ if (!customerId) {
1343
+ handleError("Customer ID is required");
1344
+ return;
1345
+ }
1346
+ setLoading(true);
1347
+ setError(null);
1348
+ try {
1349
+ const customerData = await fetchCustomer(customerId);
1350
+ setCustomer(customerData);
1351
+ } finally {
1352
+ setLoading(false);
1353
+ }
1354
+ }, [customerId, fetchCustomer, handleError]);
1355
+ const createCustomer2 = useCallback3(
1356
+ async (params) => {
1357
+ if (!validateEmail(params.email)) {
1358
+ handleError("Invalid email address");
1359
+ return null;
1360
+ }
1361
+ setLoading(true);
1362
+ setError(null);
1363
+ try {
1364
+ const response = await fetch("/api/payments/customer", {
1365
+ method: "POST",
1366
+ headers: { "Content-Type": "application/json" },
1367
+ body: JSON.stringify(params)
1368
+ });
1369
+ if (!response.ok) {
1370
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1371
+ throw new Error(errorData.error || "Failed to create customer");
1372
+ }
1373
+ const customerData = await response.json();
1374
+ setCustomer(customerData);
1375
+ handleSuccess("Customer created successfully!");
1376
+ return customerData;
1377
+ } catch (err) {
1378
+ const errorMessage = err instanceof Error ? err.message : "Failed to create customer";
1379
+ handleError(errorMessage);
1380
+ return null;
1381
+ } finally {
1382
+ setLoading(false);
1383
+ }
1384
+ },
1385
+ [handleError, handleSuccess]
1386
+ );
1387
+ const updateCustomer = useCallback3(
1388
+ async (params) => {
1389
+ if (!customerId) {
1390
+ handleError("Customer ID is required");
1391
+ return false;
1392
+ }
1393
+ if (params.email && !validateEmail(params.email)) {
1394
+ handleError("Invalid email address");
1395
+ return false;
1396
+ }
1397
+ setLoading(true);
1398
+ setError(null);
1399
+ try {
1400
+ const response = await fetch(`/api/payments/customer/${customerId}`, {
1401
+ method: "PUT",
1402
+ headers: { "Content-Type": "application/json" },
1403
+ body: JSON.stringify(params)
1404
+ });
1405
+ if (!response.ok) {
1406
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1407
+ throw new Error(errorData.error || "Failed to update customer");
1408
+ }
1409
+ await refetch();
1410
+ handleSuccess("Customer updated successfully!");
1411
+ return true;
1412
+ } catch (err) {
1413
+ const errorMessage = err instanceof Error ? err.message : "Failed to update customer";
1414
+ handleError(errorMessage);
1415
+ return false;
1416
+ } finally {
1417
+ setLoading(false);
1418
+ }
1419
+ },
1420
+ [customerId, refetch, handleError, handleSuccess]
1421
+ );
1422
+ const deleteCustomer = useCallback3(async () => {
1423
+ if (!customerId) {
1424
+ handleError("Customer ID is required");
1425
+ return false;
1426
+ }
1427
+ return new Promise((resolve) => {
1428
+ Alert5.alert(
1429
+ "Delete Customer",
1430
+ "Are you sure you want to delete this customer? This action cannot be undone.",
1431
+ [
1432
+ {
1433
+ text: "Cancel",
1434
+ style: "cancel",
1435
+ onPress: () => resolve(false)
1436
+ },
1437
+ {
1438
+ text: "Delete",
1439
+ style: "destructive",
1440
+ onPress: async () => {
1441
+ setLoading(true);
1442
+ setError(null);
1443
+ try {
1444
+ const response = await fetch(
1445
+ `/api/payments/customer/${customerId}`,
1446
+ {
1447
+ method: "DELETE"
1448
+ }
1449
+ );
1450
+ if (!response.ok) {
1451
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1452
+ throw new Error(
1453
+ errorData.error || "Failed to delete customer"
1454
+ );
1455
+ }
1456
+ setCustomer(null);
1457
+ setPaymentMethods([]);
1458
+ handleSuccess("Customer deleted successfully!");
1459
+ resolve(true);
1460
+ } catch (err) {
1461
+ const errorMessage = err instanceof Error ? err.message : "Failed to delete customer";
1462
+ handleError(errorMessage);
1463
+ resolve(false);
1464
+ } finally {
1465
+ setLoading(false);
1466
+ }
1467
+ }
1468
+ }
1469
+ ]
1470
+ );
1471
+ });
1472
+ }, [customerId, handleError, handleSuccess]);
1473
+ const fetchPaymentMethods = useCallback3(async () => {
1474
+ if (!customerId) {
1475
+ handleError("Customer ID is required");
1476
+ return [];
1477
+ }
1478
+ try {
1479
+ const response = await fetch(
1480
+ `/api/payments/customer/${customerId}/payment-methods`
1481
+ );
1482
+ if (!response.ok) {
1483
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1484
+ throw new Error(errorData.error || "Failed to fetch payment methods");
1485
+ }
1486
+ const data = await response.json();
1487
+ const methods = data.paymentMethods || [];
1488
+ setPaymentMethods(methods);
1489
+ return methods;
1490
+ } catch (err) {
1491
+ const errorMessage = err instanceof Error ? err.message : "Failed to fetch payment methods";
1492
+ handleError(errorMessage);
1493
+ return [];
1494
+ }
1495
+ }, [customerId, handleError]);
1496
+ const addPaymentMethod = useCallback3(
1497
+ async (paymentMethodId) => {
1498
+ if (!customerId) {
1499
+ handleError("Customer ID is required");
1500
+ return false;
1501
+ }
1502
+ try {
1503
+ const response = await fetch(
1504
+ `/api/payments/customer/${customerId}/payment-methods`,
1505
+ {
1506
+ method: "POST",
1507
+ headers: { "Content-Type": "application/json" },
1508
+ body: JSON.stringify({ paymentMethodId })
1509
+ }
1510
+ );
1511
+ if (!response.ok) {
1512
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1513
+ throw new Error(errorData.error || "Failed to add payment method");
1514
+ }
1515
+ await fetchPaymentMethods();
1516
+ handleSuccess("Payment method added successfully!");
1517
+ return true;
1518
+ } catch (err) {
1519
+ const errorMessage = err instanceof Error ? err.message : "Failed to add payment method";
1520
+ handleError(errorMessage);
1521
+ return false;
1522
+ }
1523
+ },
1524
+ [customerId, fetchPaymentMethods, handleError, handleSuccess]
1525
+ );
1526
+ const removePaymentMethod = useCallback3(
1527
+ async (paymentMethodId) => {
1528
+ if (!customerId) {
1529
+ handleError("Customer ID is required");
1530
+ return false;
1531
+ }
1532
+ return new Promise((resolve) => {
1533
+ Alert5.alert(
1534
+ "Remove Payment Method",
1535
+ "Are you sure you want to remove this payment method?",
1536
+ [
1537
+ {
1538
+ text: "Cancel",
1539
+ style: "cancel",
1540
+ onPress: () => resolve(false)
1541
+ },
1542
+ {
1543
+ text: "Remove",
1544
+ style: "destructive",
1545
+ onPress: async () => {
1546
+ try {
1547
+ const response = await fetch(
1548
+ `/api/payments/customer/${customerId}/payment-methods/${paymentMethodId}`,
1549
+ {
1550
+ method: "DELETE"
1551
+ }
1552
+ );
1553
+ if (!response.ok) {
1554
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1555
+ throw new Error(
1556
+ errorData.error || "Failed to remove payment method"
1557
+ );
1558
+ }
1559
+ await fetchPaymentMethods();
1560
+ handleSuccess("Payment method removed successfully!");
1561
+ resolve(true);
1562
+ } catch (err) {
1563
+ const errorMessage = err instanceof Error ? err.message : "Failed to remove payment method";
1564
+ handleError(errorMessage);
1565
+ resolve(false);
1566
+ }
1567
+ }
1568
+ }
1569
+ ]
1570
+ );
1571
+ });
1572
+ },
1573
+ [customerId, fetchPaymentMethods, handleError, handleSuccess]
1574
+ );
1575
+ const setDefaultPaymentMethod = useCallback3(
1576
+ async (paymentMethodId) => {
1577
+ if (!customerId) {
1578
+ handleError("Customer ID is required");
1579
+ return false;
1580
+ }
1581
+ try {
1582
+ const response = await fetch(
1583
+ `/api/payments/customer/${customerId}/default-payment-method`,
1584
+ {
1585
+ method: "PUT",
1586
+ headers: { "Content-Type": "application/json" },
1587
+ body: JSON.stringify({ paymentMethodId })
1588
+ }
1589
+ );
1590
+ if (!response.ok) {
1591
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1592
+ throw new Error(
1593
+ errorData.error || "Failed to set default payment method"
1594
+ );
1595
+ }
1596
+ await Promise.all([refetch(), fetchPaymentMethods()]);
1597
+ handleSuccess("Default payment method updated successfully!");
1598
+ return true;
1599
+ } catch (err) {
1600
+ const errorMessage = err instanceof Error ? err.message : "Failed to set default payment method";
1601
+ handleError(errorMessage);
1602
+ return false;
1603
+ }
1604
+ },
1605
+ [customerId, refetch, fetchPaymentMethods, handleError, handleSuccess]
1606
+ );
1607
+ useEffect2(() => {
1608
+ if (autoFetch && customerId) {
1609
+ refetch();
1610
+ fetchPaymentMethods();
1611
+ }
1612
+ }, [autoFetch, customerId, refetch, fetchPaymentMethods]);
1613
+ return {
1614
+ customer,
1615
+ paymentMethods,
1616
+ loading,
1617
+ error,
1618
+ refetch,
1619
+ createCustomer: createCustomer2,
1620
+ updateCustomer,
1621
+ deleteCustomer,
1622
+ fetchPaymentMethods,
1623
+ addPaymentMethod,
1624
+ removePaymentMethod,
1625
+ setDefaultPaymentMethod
1626
+ };
1627
+ };
1628
+
1629
+ // src/native/utils/stripe-native.ts
1630
+ var createSubscriptionSetup = async (params) => {
1631
+ try {
1632
+ const response = await fetch("/api/payments/create-subscription-setup", {
1633
+ method: "POST",
1634
+ headers: { "Content-Type": "application/json" },
1635
+ body: JSON.stringify(params)
1636
+ });
1637
+ if (!response.ok) {
1638
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1639
+ return {
1640
+ error: errorData.error || "Failed to create subscription setup"
1641
+ };
1642
+ }
1643
+ const data = await response.json();
1644
+ return {
1645
+ setupIntentClientSecret: data.setupIntentClientSecret,
1646
+ customerId: data.customerId,
1647
+ customerEphemeralKeySecret: data.customerEphemeralKeySecret
1648
+ };
1649
+ } catch (error) {
1650
+ return {
1651
+ error: error instanceof Error ? error.message : "Failed to create subscription setup"
1652
+ };
1653
+ }
1654
+ };
1655
+ var createPaymentIntent = async (params) => {
1656
+ try {
1657
+ const response = await fetch("/api/payments/create-payment-intent", {
1658
+ method: "POST",
1659
+ headers: { "Content-Type": "application/json" },
1660
+ body: JSON.stringify(params)
1661
+ });
1662
+ if (!response.ok) {
1663
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1664
+ return { error: errorData.error || "Failed to create payment intent" };
1665
+ }
1666
+ const data = await response.json();
1667
+ return { clientSecret: data.client_secret };
1668
+ } catch (error) {
1669
+ return {
1670
+ error: error instanceof Error ? error.message : "Failed to create payment intent"
1671
+ };
1672
+ }
1673
+ };
1674
+ var validatePaymentsConfig = (config) => {
1675
+ const errors = [];
1676
+ if (!config.publishableKey) {
1677
+ errors.push("publishableKey is required");
1678
+ }
1679
+ if (!config.provider) {
1680
+ errors.push("provider is required");
1681
+ }
1682
+ if (config.provider !== "stripe") {
1683
+ errors.push("Only stripe provider is currently supported");
1684
+ }
1685
+ if (!config.environment) {
1686
+ errors.push("environment is required");
1687
+ }
1688
+ if (config.environment && !["development", "production"].includes(config.environment)) {
1689
+ errors.push('environment must be either "development" or "production"');
1690
+ }
1691
+ if (config.publishableKey && !config.publishableKey.startsWith("pk_")) {
1692
+ errors.push('publishableKey must start with "pk_"');
1693
+ }
1694
+ return {
1695
+ isValid: errors.length === 0,
1696
+ errors
1697
+ };
1698
+ };
1699
+ var formatStripeError = (error) => {
1700
+ if (typeof error === "string") {
1701
+ return error;
1702
+ }
1703
+ if (error?.message) {
1704
+ return error.message;
1705
+ }
1706
+ if (error?.code) {
1707
+ const errorMessages = {
1708
+ Canceled: "Payment was cancelled",
1709
+ Failed: "Payment failed",
1710
+ card_declined: "Your card was declined.",
1711
+ expired_card: "Your card has expired.",
1712
+ incorrect_cvc: "Your card's security code is incorrect.",
1713
+ processing_error: "An error occurred while processing your card.",
1714
+ incorrect_number: "Your card number is incorrect."
1715
+ };
1716
+ return errorMessages[error.code] || `Payment failed: ${error.code}`;
1717
+ }
1718
+ return "An unexpected error occurred.";
1719
+ };
1720
+ var createCustomer = async (params) => {
1721
+ try {
1722
+ const response = await fetch("/api/payments/customer", {
1723
+ method: "POST",
1724
+ headers: { "Content-Type": "application/json" },
1725
+ body: JSON.stringify(params)
1726
+ });
1727
+ if (!response.ok) {
1728
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1729
+ return { error: errorData.error || "Failed to create customer" };
1730
+ }
1731
+ const customer = await response.json();
1732
+ return { customer };
1733
+ } catch (error) {
1734
+ return {
1735
+ error: error instanceof Error ? error.message : "Failed to create customer"
1736
+ };
1737
+ }
1738
+ };
1739
+ var fetchSubscription = async (subscriptionId) => {
1740
+ try {
1741
+ const response = await fetch(
1742
+ `/api/payments/subscription/${subscriptionId}`
1743
+ );
1744
+ if (!response.ok) {
1745
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1746
+ return {
1747
+ error: errorData.error || ERROR_MESSAGES.SUBSCRIPTION_NOT_FOUND
1748
+ };
1749
+ }
1750
+ const subscription = await response.json();
1751
+ return { subscription };
1752
+ } catch (error) {
1753
+ return {
1754
+ error: error instanceof Error ? error.message : ERROR_MESSAGES.SUBSCRIPTION_NOT_FOUND
1755
+ };
1756
+ }
1757
+ };
1758
+ var fetchCustomerSubscriptions = async (customerId) => {
1759
+ try {
1760
+ const response = await fetch(
1761
+ `/api/payments/customer/${customerId}/subscriptions`
1762
+ );
1763
+ if (!response.ok) {
1764
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1765
+ return { error: errorData.error || ERROR_MESSAGES.CUSTOMER_NOT_FOUND };
1766
+ }
1767
+ const data = await response.json();
1768
+ return { subscriptions: data.subscriptions || [] };
1769
+ } catch (error) {
1770
+ return {
1771
+ error: error instanceof Error ? error.message : ERROR_MESSAGES.CUSTOMER_NOT_FOUND
1772
+ };
1773
+ }
1774
+ };
1775
+ var cancelSubscription = async (subscriptionId) => {
1776
+ try {
1777
+ const response = await fetch(
1778
+ `/api/payments/subscription/${subscriptionId}/cancel`,
1779
+ {
1780
+ method: "POST",
1781
+ headers: { "Content-Type": "application/json" }
1782
+ }
1783
+ );
1784
+ if (!response.ok) {
1785
+ const errorData = await response.json().catch(() => ({ error: "Network error" }));
1786
+ return {
1787
+ success: false,
1788
+ error: errorData.error || "Failed to cancel subscription"
1789
+ };
1790
+ }
1791
+ return { success: true };
1792
+ } catch (error) {
1793
+ return {
1794
+ success: false,
1795
+ error: error instanceof Error ? error.message : "Failed to cancel subscription"
1796
+ };
1797
+ }
1798
+ };
1799
+ export {
1800
+ CheckoutButton,
1801
+ PaymentForm,
1802
+ PricingTable,
1803
+ StripeProvider,
1804
+ cancelSubscription,
1805
+ createCustomer,
1806
+ createPaymentIntent,
1807
+ createSubscriptionSetup,
1808
+ fetchCustomerSubscriptions,
1809
+ fetchSubscription,
1810
+ formatStripeError,
1811
+ useCheckout,
1812
+ useCustomer,
1813
+ useSubscription,
1814
+ validatePaymentsConfig
1815
+ };