@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/LICENSE +22 -0
- package/README.md +679 -0
- package/dist/index.d.mts +365 -0
- package/dist/index.d.ts +365 -0
- package/dist/index.js +869 -0
- package/dist/index.mjs +806 -0
- package/dist/native/index.d.mts +266 -0
- package/dist/native/index.d.ts +266 -0
- package/dist/native/index.js +1833 -0
- package/dist/native/index.mjs +1815 -0
- package/dist/server/index.d.mts +122 -0
- package/dist/server/index.d.ts +122 -0
- package/dist/server/index.js +484 -0
- package/dist/server/index.mjs +443 -0
- package/dist/web/index.d.mts +281 -0
- package/dist/web/index.d.ts +281 -0
- package/dist/web/index.js +1651 -0
- package/dist/web/index.mjs +1604 -0
- package/package.json +153 -0
|
@@ -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
|
+
};
|