@solvapay/react 1.0.0-preview.4 → 1.0.0-preview.6
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/README.md +148 -15
- package/dist/index.cjs +432 -171
- package/dist/index.d.cts +216 -60
- package/dist/index.d.ts +216 -60
- package/dist/index.js +427 -171
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,177 +1,253 @@
|
|
|
1
1
|
// src/SolvaPayProvider.tsx
|
|
2
|
-
import { useState, useEffect } from "react";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useState, useEffect, useCallback, useMemo } from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
var SolvaPayContext = createContext(null);
|
|
6
5
|
var SolvaPayProvider = ({
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
agentRef,
|
|
13
|
-
apiBaseUrl = "/api/solvapay",
|
|
14
|
-
onPaymentReady
|
|
6
|
+
createPayment,
|
|
7
|
+
checkSubscription,
|
|
8
|
+
customerRef,
|
|
9
|
+
onCustomerRefUpdate,
|
|
10
|
+
children
|
|
15
11
|
}) => {
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
if (!createPayment || typeof createPayment !== "function") {
|
|
13
|
+
throw new Error("SolvaPayProvider: createPayment prop is required and must be a function");
|
|
14
|
+
}
|
|
15
|
+
if (!checkSubscription || typeof checkSubscription !== "function") {
|
|
16
|
+
throw new Error("SolvaPayProvider: checkSubscription prop is required and must be a function");
|
|
17
|
+
}
|
|
18
|
+
const [subscriptionData, setSubscriptionData] = useState({
|
|
19
|
+
subscriptions: []
|
|
20
|
+
});
|
|
18
21
|
const [loading, setLoading] = useState(false);
|
|
19
|
-
const [
|
|
20
|
-
|
|
22
|
+
const [internalCustomerRef, setInternalCustomerRef] = useState(customerRef);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (customerRef && customerRef !== internalCustomerRef) {
|
|
25
|
+
setInternalCustomerRef(customerRef);
|
|
26
|
+
}
|
|
27
|
+
}, [customerRef, internalCustomerRef]);
|
|
28
|
+
const fetchSubscription = useCallback(async () => {
|
|
29
|
+
const currentCustomerRef = internalCustomerRef;
|
|
30
|
+
if (!currentCustomerRef) {
|
|
31
|
+
setSubscriptionData({ subscriptions: [] });
|
|
32
|
+
setLoading(false);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
21
35
|
setLoading(true);
|
|
22
|
-
setError(null);
|
|
23
36
|
try {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
planRef,
|
|
32
|
-
agentRef,
|
|
33
|
-
amount: paymentAmount,
|
|
34
|
-
currency,
|
|
35
|
-
description: "SolvaPay - Plan Purchase"
|
|
36
|
-
})
|
|
37
|
+
const data = await checkSubscription(currentCustomerRef);
|
|
38
|
+
setSubscriptionData(data);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error("\u274C [SolvaPayProvider] Failed to fetch subscription:", error);
|
|
41
|
+
setSubscriptionData({
|
|
42
|
+
customerRef: currentCustomerRef,
|
|
43
|
+
subscriptions: []
|
|
37
44
|
});
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
} finally {
|
|
46
|
+
setLoading(false);
|
|
47
|
+
}
|
|
48
|
+
}, [internalCustomerRef, checkSubscription]);
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
fetchSubscription();
|
|
51
|
+
}, [fetchSubscription]);
|
|
52
|
+
const updateCustomerRef = useCallback((newCustomerRef) => {
|
|
53
|
+
const previousRef = internalCustomerRef;
|
|
54
|
+
setInternalCustomerRef(newCustomerRef);
|
|
55
|
+
if (onCustomerRefUpdate) {
|
|
56
|
+
onCustomerRefUpdate(newCustomerRef);
|
|
57
|
+
}
|
|
58
|
+
if (previousRef !== newCustomerRef) {
|
|
59
|
+
}
|
|
60
|
+
}, [onCustomerRefUpdate, internalCustomerRef]);
|
|
61
|
+
const subscription = useMemo(() => ({
|
|
62
|
+
loading,
|
|
63
|
+
customerRef: subscriptionData.customerRef,
|
|
64
|
+
email: subscriptionData.email,
|
|
65
|
+
name: subscriptionData.name,
|
|
66
|
+
subscriptions: subscriptionData.subscriptions,
|
|
67
|
+
hasActiveSubscription: subscriptionData.subscriptions.some(
|
|
68
|
+
(sub) => sub.status === "active"
|
|
69
|
+
),
|
|
70
|
+
hasPlan: (planName) => {
|
|
71
|
+
return subscriptionData.subscriptions.some(
|
|
72
|
+
(sub) => sub.planName.toLowerCase() === planName.toLowerCase() && sub.status === "active"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}), [loading, subscriptionData]);
|
|
76
|
+
const contextValue = useMemo(() => ({
|
|
77
|
+
subscription,
|
|
78
|
+
refetchSubscription: fetchSubscription,
|
|
79
|
+
createPayment,
|
|
80
|
+
customerRef: internalCustomerRef,
|
|
81
|
+
updateCustomerRef
|
|
82
|
+
}), [subscription, fetchSubscription, createPayment, internalCustomerRef, updateCustomerRef]);
|
|
83
|
+
return /* @__PURE__ */ jsx(SolvaPayContext.Provider, { value: contextValue, children });
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// src/PaymentForm.tsx
|
|
87
|
+
import { useState as useState3, useEffect as useEffect2, Suspense, useRef as useRef2 } from "react";
|
|
88
|
+
import { useStripe, useElements, PaymentElement, Elements } from "@stripe/react-stripe-js";
|
|
89
|
+
|
|
90
|
+
// src/hooks/useCheckout.ts
|
|
91
|
+
import { useState as useState2, useCallback as useCallback2, useRef } from "react";
|
|
92
|
+
import { loadStripe } from "@stripe/stripe-js";
|
|
93
|
+
|
|
94
|
+
// src/hooks/useSolvaPay.ts
|
|
95
|
+
import { useContext } from "react";
|
|
96
|
+
function useSolvaPay() {
|
|
97
|
+
const context = useContext(SolvaPayContext);
|
|
98
|
+
if (!context) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
"useSolvaPay must be used within a SolvaPayProvider. Wrap your component tree with <SolvaPayProvider> to use this hook."
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return context;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/hooks/useCheckout.ts
|
|
107
|
+
function useCheckout(planRef) {
|
|
108
|
+
const { createPayment, customerRef, updateCustomerRef } = useSolvaPay();
|
|
109
|
+
const [loading, setLoading] = useState2(false);
|
|
110
|
+
const [error, setError] = useState2(
|
|
111
|
+
!planRef || typeof planRef !== "string" ? new Error("useCheckout: planRef parameter is required and must be a string") : null
|
|
112
|
+
);
|
|
113
|
+
const [stripePromise, setStripePromise] = useState2(null);
|
|
114
|
+
const [clientSecret, setClientSecret] = useState2(null);
|
|
115
|
+
const isStartingRef = useRef(false);
|
|
116
|
+
const startCheckout = useCallback2(async () => {
|
|
117
|
+
if (isStartingRef.current || loading) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!planRef || typeof planRef !== "string") {
|
|
121
|
+
setError(new Error("useCheckout: planRef parameter is required and must be a string"));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (!customerRef) {
|
|
125
|
+
setError(new Error("No customer reference available. Please ensure you are logged in."));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
isStartingRef.current = true;
|
|
129
|
+
setLoading(true);
|
|
130
|
+
setError(null);
|
|
131
|
+
try {
|
|
132
|
+
const result = await createPayment({ planRef, customerRef });
|
|
133
|
+
if (!result || typeof result !== "object") {
|
|
134
|
+
throw new Error("Invalid payment intent response from server");
|
|
45
135
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
hasPublishableKey: !!result.publishableKey,
|
|
49
|
-
hasAccountId: !!result.accountId,
|
|
50
|
-
clientSecretLength: result.clientSecret?.length || 0,
|
|
51
|
-
publishableKeyLength: result.publishableKey?.length || 0
|
|
52
|
-
});
|
|
53
|
-
const stripeOptions = result.accountId ? { stripeAccount: result.accountId } : {};
|
|
54
|
-
const stripe = await loadStripe(result.publishableKey, stripeOptions);
|
|
55
|
-
if (!stripe) {
|
|
56
|
-
throw new Error("Failed to initialize Stripe. Please check your configuration.");
|
|
136
|
+
if (!result.clientSecret || typeof result.clientSecret !== "string") {
|
|
137
|
+
throw new Error("Invalid client secret in payment intent response");
|
|
57
138
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (onPaymentReady) {
|
|
61
|
-
onPaymentReady(stripe, result.clientSecret);
|
|
139
|
+
if (!result.publishableKey || typeof result.publishableKey !== "string") {
|
|
140
|
+
throw new Error("Invalid publishable key in payment intent response");
|
|
62
141
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
} else if (error2.message.includes("Failed to initialize Stripe")) {
|
|
72
|
-
errorMessage = "Stripe initialization failed. Please check your configuration.";
|
|
142
|
+
if (result.customerRef && result.customerRef !== customerRef) {
|
|
143
|
+
console.log(`\u{1F504} [useCheckout] Backend returned different customerRef:`, {
|
|
144
|
+
sent: customerRef,
|
|
145
|
+
received: result.customerRef
|
|
146
|
+
});
|
|
147
|
+
if (updateCustomerRef) {
|
|
148
|
+
updateCustomerRef(result.customerRef);
|
|
149
|
+
console.log("\u2705 [useCheckout] Customer reference updated via callback");
|
|
73
150
|
} else {
|
|
74
|
-
|
|
151
|
+
console.warn("\u26A0\uFE0F [useCheckout] No updateCustomerRef callback available!");
|
|
75
152
|
}
|
|
153
|
+
} else if (result.customerRef === customerRef) {
|
|
154
|
+
console.log("\u2705 [useCheckout] Customer reference unchanged:", customerRef);
|
|
76
155
|
}
|
|
77
|
-
|
|
156
|
+
const stripeOptions = result.accountId ? { stripeAccount: result.accountId } : {};
|
|
157
|
+
const stripe = loadStripe(result.publishableKey, stripeOptions);
|
|
158
|
+
setStripePromise(stripe);
|
|
159
|
+
setClientSecret(result.clientSecret);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
const error2 = err instanceof Error ? err : new Error("Failed to start checkout");
|
|
162
|
+
setError(error2);
|
|
78
163
|
} finally {
|
|
79
164
|
setLoading(false);
|
|
165
|
+
isStartingRef.current = false;
|
|
80
166
|
}
|
|
167
|
+
}, [planRef, customerRef, createPayment, updateCustomerRef, loading]);
|
|
168
|
+
const reset = useCallback2(() => {
|
|
169
|
+
isStartingRef.current = false;
|
|
170
|
+
setLoading(false);
|
|
171
|
+
setError(null);
|
|
172
|
+
setStripePromise(null);
|
|
173
|
+
setClientSecret(null);
|
|
174
|
+
}, []);
|
|
175
|
+
return {
|
|
176
|
+
loading,
|
|
177
|
+
error,
|
|
178
|
+
stripePromise,
|
|
179
|
+
clientSecret,
|
|
180
|
+
startCheckout,
|
|
181
|
+
reset
|
|
81
182
|
};
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
borderRadius: "0.375rem",
|
|
93
|
-
marginBottom: "1rem"
|
|
94
|
-
}, children: [
|
|
95
|
-
/* @__PURE__ */ jsx("strong", { children: "Payment Setup Error:" }),
|
|
96
|
-
" ",
|
|
97
|
-
error,
|
|
98
|
-
/* @__PURE__ */ jsx(
|
|
99
|
-
"button",
|
|
100
|
-
{
|
|
101
|
-
onClick: () => createPayment(amount),
|
|
102
|
-
style: {
|
|
103
|
-
marginLeft: "1rem",
|
|
104
|
-
padding: "0.5rem 1rem",
|
|
105
|
-
backgroundColor: "#742a2a",
|
|
106
|
-
color: "white",
|
|
107
|
-
border: "none",
|
|
108
|
-
borderRadius: "0.25rem",
|
|
109
|
-
cursor: "pointer"
|
|
110
|
-
},
|
|
111
|
-
children: "Retry"
|
|
112
|
-
}
|
|
113
|
-
)
|
|
114
|
-
] });
|
|
115
|
-
}
|
|
116
|
-
if (loading) {
|
|
117
|
-
return /* @__PURE__ */ jsx("div", { style: {
|
|
118
|
-
padding: "1rem",
|
|
119
|
-
textAlign: "center",
|
|
120
|
-
color: "#4a5568"
|
|
121
|
-
}, children: "Setting up payment..." });
|
|
122
|
-
}
|
|
123
|
-
if (!stripePromise || !clientSecret) {
|
|
124
|
-
return /* @__PURE__ */ jsx("div", { style: {
|
|
125
|
-
padding: "1rem",
|
|
126
|
-
textAlign: "center",
|
|
127
|
-
color: "#4a5568"
|
|
128
|
-
}, children: /* @__PURE__ */ jsx(
|
|
129
|
-
"button",
|
|
130
|
-
{
|
|
131
|
-
onClick: () => createPayment(amount),
|
|
132
|
-
style: {
|
|
133
|
-
padding: "0.75rem 1.5rem",
|
|
134
|
-
backgroundColor: "#3182ce",
|
|
135
|
-
color: "white",
|
|
136
|
-
border: "none",
|
|
137
|
-
borderRadius: "0.375rem",
|
|
138
|
-
cursor: "pointer",
|
|
139
|
-
fontSize: "1rem"
|
|
140
|
-
},
|
|
141
|
-
children: "Initialize Payment"
|
|
142
|
-
}
|
|
143
|
-
) });
|
|
144
|
-
}
|
|
145
|
-
return /* @__PURE__ */ jsx(Elements, { stripe: stripePromise, options: { clientSecret }, children });
|
|
146
|
-
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/hooks/useSubscription.ts
|
|
186
|
+
function useSubscription() {
|
|
187
|
+
const { subscription, refetchSubscription } = useSolvaPay();
|
|
188
|
+
return {
|
|
189
|
+
...subscription,
|
|
190
|
+
refetch: refetchSubscription
|
|
191
|
+
};
|
|
192
|
+
}
|
|
147
193
|
|
|
148
194
|
// src/PaymentForm.tsx
|
|
149
|
-
import {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
195
|
+
import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
196
|
+
var Spinner = ({
|
|
197
|
+
className = "",
|
|
198
|
+
size = "md"
|
|
199
|
+
}) => {
|
|
200
|
+
const sizeClasses = {
|
|
201
|
+
sm: "w-4 h-4 border-2",
|
|
202
|
+
md: "w-6 h-6 border-2",
|
|
203
|
+
lg: "w-8 h-8 border-2"
|
|
204
|
+
};
|
|
205
|
+
return /* @__PURE__ */ jsx2(
|
|
206
|
+
"div",
|
|
207
|
+
{
|
|
208
|
+
className: `inline-block border-gray-300 border-t-gray-600 rounded-full animate-spin ${sizeClasses[size]} ${className}`,
|
|
209
|
+
role: "status",
|
|
210
|
+
"aria-busy": "true"
|
|
211
|
+
}
|
|
212
|
+
);
|
|
213
|
+
};
|
|
214
|
+
var StripePaymentFormWrapper = (props) => {
|
|
215
|
+
const stripe = useStripe();
|
|
216
|
+
const elements = useElements();
|
|
217
|
+
if (!stripe || !elements) {
|
|
218
|
+
return /* @__PURE__ */ jsx2("div", { className: "space-y-4", children: /* @__PURE__ */ jsx2("div", { className: "p-4 bg-gray-50 border border-gray-200 rounded-lg flex items-center justify-center", children: /* @__PURE__ */ jsx2(Spinner, { size: "md" }) }) });
|
|
219
|
+
}
|
|
220
|
+
return /* @__PURE__ */ jsx2(StripePaymentForm, { ...props });
|
|
221
|
+
};
|
|
222
|
+
var StripePaymentForm = ({
|
|
153
223
|
onSuccess,
|
|
154
224
|
onError,
|
|
155
225
|
returnUrl,
|
|
156
226
|
submitButtonText = "Pay Now",
|
|
157
|
-
|
|
227
|
+
buttonClassName
|
|
158
228
|
}) => {
|
|
159
229
|
const stripe = useStripe();
|
|
160
230
|
const elements = useElements();
|
|
161
|
-
const [isProcessing, setIsProcessing] =
|
|
162
|
-
const [message, setMessage] =
|
|
231
|
+
const [isProcessing, setIsProcessing] = useState3(false);
|
|
232
|
+
const [message, setMessage] = useState3(null);
|
|
163
233
|
const handleSubmit = async (event) => {
|
|
164
234
|
event.preventDefault();
|
|
165
235
|
if (!stripe || !elements) {
|
|
236
|
+
const errorMessage = "Stripe is not available. Please refresh the page.";
|
|
237
|
+
setMessage(errorMessage);
|
|
238
|
+
if (onError) {
|
|
239
|
+
onError(new Error(errorMessage));
|
|
240
|
+
}
|
|
166
241
|
return;
|
|
167
242
|
}
|
|
168
243
|
setIsProcessing(true);
|
|
169
244
|
setMessage(null);
|
|
170
245
|
try {
|
|
246
|
+
const finalReturnUrl = returnUrl || (typeof window !== "undefined" ? window.location.href : "/");
|
|
171
247
|
const { error, paymentIntent } = await stripe.confirmPayment({
|
|
172
248
|
elements,
|
|
173
249
|
confirmParams: {
|
|
174
|
-
return_url:
|
|
250
|
+
return_url: finalReturnUrl
|
|
175
251
|
},
|
|
176
252
|
redirect: "if_required"
|
|
177
253
|
// Only redirect if required by payment method
|
|
@@ -187,8 +263,8 @@ var PaymentForm = ({
|
|
|
187
263
|
if (onSuccess) {
|
|
188
264
|
onSuccess(paymentIntent);
|
|
189
265
|
}
|
|
190
|
-
} else {
|
|
191
|
-
setMessage(`Payment status: ${paymentIntent
|
|
266
|
+
} else if (paymentIntent) {
|
|
267
|
+
setMessage(`Payment status: ${paymentIntent.status || "processing"}`);
|
|
192
268
|
}
|
|
193
269
|
} catch (err) {
|
|
194
270
|
const error = err instanceof Error ? err : new Error("Unknown error occurred");
|
|
@@ -200,45 +276,225 @@ var PaymentForm = ({
|
|
|
200
276
|
setIsProcessing(false);
|
|
201
277
|
}
|
|
202
278
|
};
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
279
|
+
const isSuccess = message?.includes("successful") ?? false;
|
|
280
|
+
return /* @__PURE__ */ jsxs(
|
|
281
|
+
"form",
|
|
282
|
+
{
|
|
283
|
+
onSubmit: handleSubmit,
|
|
284
|
+
className: "space-y-6",
|
|
285
|
+
noValidate: true,
|
|
286
|
+
children: [
|
|
287
|
+
/* @__PURE__ */ jsx2(PaymentElement, {}),
|
|
288
|
+
message && /* @__PURE__ */ jsx2(
|
|
289
|
+
"div",
|
|
290
|
+
{
|
|
291
|
+
className: `mt-6 p-4 rounded-lg ${isSuccess ? "bg-green-50 border border-green-400 text-green-700" : "bg-red-50 border border-red-400 text-red-700"}`,
|
|
292
|
+
role: isSuccess ? "status" : "alert",
|
|
293
|
+
"aria-live": isSuccess ? "polite" : "assertive",
|
|
294
|
+
"aria-atomic": "true",
|
|
295
|
+
children: message
|
|
296
|
+
}
|
|
297
|
+
),
|
|
298
|
+
/* @__PURE__ */ jsx2(
|
|
299
|
+
"button",
|
|
300
|
+
{
|
|
301
|
+
type: "submit",
|
|
302
|
+
disabled: !stripe || isProcessing,
|
|
303
|
+
className: buttonClassName,
|
|
304
|
+
"aria-busy": isProcessing,
|
|
305
|
+
"aria-disabled": !stripe || isProcessing,
|
|
306
|
+
children: isProcessing ? /* @__PURE__ */ jsx2("span", { className: "flex items-center justify-center gap-2", children: /* @__PURE__ */ jsx2(Spinner, { size: "sm" }) }) : submitButtonText
|
|
307
|
+
}
|
|
308
|
+
)
|
|
309
|
+
]
|
|
310
|
+
}
|
|
311
|
+
);
|
|
312
|
+
};
|
|
313
|
+
var PaymentForm = ({
|
|
314
|
+
planRef,
|
|
315
|
+
onSuccess,
|
|
316
|
+
onError,
|
|
317
|
+
returnUrl,
|
|
318
|
+
submitButtonText = "Pay Now",
|
|
319
|
+
className,
|
|
320
|
+
buttonClassName,
|
|
321
|
+
initialButtonText,
|
|
322
|
+
cancelButtonText = "Cancel"
|
|
323
|
+
}) => {
|
|
324
|
+
const validPlanRef = planRef && typeof planRef === "string" ? planRef : "";
|
|
325
|
+
const { loading, error, stripePromise, clientSecret, startCheckout, reset } = useCheckout(validPlanRef);
|
|
326
|
+
const { refetch } = useSubscription();
|
|
327
|
+
const [showPaymentForm, setShowPaymentForm] = useState3(false);
|
|
328
|
+
const [hasStartedCheckout, setHasStartedCheckout] = useState3(false);
|
|
329
|
+
const initializationKey = useRef2("");
|
|
330
|
+
const planRefError = !planRef || typeof planRef !== "string" ? new Error("PaymentForm: planRef is required") : null;
|
|
331
|
+
if (planRefError) {
|
|
332
|
+
return /* @__PURE__ */ jsx2("div", { className, children: /* @__PURE__ */ jsx2("div", { className: "mt-4 p-4 bg-red-50 border border-red-400 rounded-lg text-red-700", children: planRefError.message }) });
|
|
333
|
+
}
|
|
334
|
+
useEffect2(() => {
|
|
335
|
+
const key = `${planRef}-${initialButtonText}`;
|
|
336
|
+
if (initializationKey.current === key) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
setShowPaymentForm(false);
|
|
340
|
+
setHasStartedCheckout(false);
|
|
341
|
+
reset();
|
|
342
|
+
initializationKey.current = key;
|
|
343
|
+
if (!initialButtonText && planRef && !loading && !error) {
|
|
344
|
+
requestAnimationFrame(() => {
|
|
345
|
+
setHasStartedCheckout(true);
|
|
346
|
+
startCheckout().then(() => {
|
|
347
|
+
setShowPaymentForm(true);
|
|
348
|
+
}).catch(() => {
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}, [planRef, initialButtonText, loading, error, reset]);
|
|
353
|
+
const handleStartCheckout = async () => {
|
|
354
|
+
setHasStartedCheckout(true);
|
|
355
|
+
await startCheckout();
|
|
356
|
+
setShowPaymentForm(true);
|
|
357
|
+
};
|
|
358
|
+
const handleSuccess = async (paymentIntent) => {
|
|
359
|
+
setShowPaymentForm(false);
|
|
360
|
+
reset();
|
|
361
|
+
await refetch();
|
|
362
|
+
if (onSuccess) {
|
|
363
|
+
onSuccess(paymentIntent);
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
const handleCancel = () => {
|
|
367
|
+
setShowPaymentForm(false);
|
|
368
|
+
reset();
|
|
369
|
+
};
|
|
370
|
+
const handleError = (err) => {
|
|
371
|
+
if (onError) {
|
|
372
|
+
onError(err);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
const finalReturnUrl = returnUrl || (typeof window !== "undefined" ? window.location.href : "/");
|
|
376
|
+
const shouldShowForm = (!initialButtonText || showPaymentForm) && stripePromise && clientSecret;
|
|
377
|
+
if (shouldShowForm) {
|
|
378
|
+
return /* @__PURE__ */ jsxs("div", { className, children: [
|
|
379
|
+
/* @__PURE__ */ jsx2(Suspense, { fallback: /* @__PURE__ */ jsx2("div", { className: "space-y-4", children: /* @__PURE__ */ jsx2("div", { className: "p-4 bg-gray-50 border border-gray-200 rounded-lg flex items-center justify-center", children: /* @__PURE__ */ jsx2(Spinner, { size: "md" }) }) }), children: /* @__PURE__ */ jsx2(
|
|
380
|
+
Elements,
|
|
381
|
+
{
|
|
382
|
+
stripe: stripePromise,
|
|
383
|
+
options: { clientSecret },
|
|
384
|
+
children: /* @__PURE__ */ jsx2(
|
|
385
|
+
StripePaymentFormWrapper,
|
|
386
|
+
{
|
|
387
|
+
onSuccess: handleSuccess,
|
|
388
|
+
onError: handleError,
|
|
389
|
+
returnUrl: finalReturnUrl,
|
|
390
|
+
submitButtonText,
|
|
391
|
+
buttonClassName
|
|
392
|
+
}
|
|
393
|
+
)
|
|
214
394
|
},
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
395
|
+
clientSecret
|
|
396
|
+
) }),
|
|
397
|
+
cancelButtonText && /* @__PURE__ */ jsx2(
|
|
398
|
+
"button",
|
|
399
|
+
{
|
|
400
|
+
onClick: handleCancel,
|
|
401
|
+
type: "button",
|
|
402
|
+
className: "mt-6 w-full px-4 py-2 text-xs text-slate-600 bg-transparent rounded-full hover:text-slate-900 hover:bg-slate-50 font-medium transition-all duration-200",
|
|
403
|
+
children: cancelButtonText
|
|
404
|
+
}
|
|
405
|
+
)
|
|
406
|
+
] });
|
|
407
|
+
}
|
|
408
|
+
return /* @__PURE__ */ jsxs("div", { className, children: [
|
|
409
|
+
loading && /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
410
|
+
/* @__PURE__ */ jsx2("div", { className: "p-4 bg-gray-50 border border-gray-200 rounded-lg flex items-center justify-center", children: /* @__PURE__ */ jsx2(Spinner, { size: "md" }) }),
|
|
411
|
+
initialButtonText && /* @__PURE__ */ jsx2(
|
|
412
|
+
"button",
|
|
413
|
+
{
|
|
414
|
+
disabled: true,
|
|
415
|
+
className: buttonClassName,
|
|
416
|
+
"aria-busy": "true",
|
|
417
|
+
children: /* @__PURE__ */ jsx2("span", { className: "flex items-center justify-center gap-2", children: /* @__PURE__ */ jsx2(Spinner, { size: "sm" }) })
|
|
418
|
+
}
|
|
419
|
+
)
|
|
420
|
+
] }),
|
|
421
|
+
error && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
422
|
+
initialButtonText && /* @__PURE__ */ jsx2(
|
|
423
|
+
"button",
|
|
424
|
+
{
|
|
425
|
+
onClick: () => {
|
|
426
|
+
reset();
|
|
427
|
+
handleStartCheckout();
|
|
428
|
+
},
|
|
429
|
+
className: buttonClassName,
|
|
430
|
+
children: initialButtonText
|
|
431
|
+
}
|
|
432
|
+
),
|
|
433
|
+
/* @__PURE__ */ jsx2("div", { className: "mt-4 p-4 bg-red-50 border border-red-400 rounded-lg text-red-700", children: error.message })
|
|
434
|
+
] }),
|
|
435
|
+
!loading && !error && initialButtonText && /* @__PURE__ */ jsx2(
|
|
219
436
|
"button",
|
|
220
437
|
{
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
marginTop: "1.5rem",
|
|
225
|
-
width: "100%",
|
|
226
|
-
padding: "0.75rem 1.5rem",
|
|
227
|
-
backgroundColor: isProcessing || !stripe ? "#9ca3af" : "#3b82f6",
|
|
228
|
-
color: "white",
|
|
229
|
-
border: "none",
|
|
230
|
-
borderRadius: "0.375rem",
|
|
231
|
-
fontSize: "1rem",
|
|
232
|
-
fontWeight: "500",
|
|
233
|
-
cursor: isProcessing || !stripe ? "not-allowed" : "pointer",
|
|
234
|
-
transition: "background-color 0.2s"
|
|
235
|
-
},
|
|
236
|
-
children: isProcessing ? "Processing..." : submitButtonText
|
|
438
|
+
onClick: handleStartCheckout,
|
|
439
|
+
className: buttonClassName,
|
|
440
|
+
children: initialButtonText
|
|
237
441
|
}
|
|
238
442
|
)
|
|
239
443
|
] });
|
|
240
444
|
};
|
|
445
|
+
|
|
446
|
+
// src/components/PlanBadge.tsx
|
|
447
|
+
import { Fragment as Fragment2, jsx as jsx3 } from "react/jsx-runtime";
|
|
448
|
+
var PlanBadge = ({
|
|
449
|
+
children,
|
|
450
|
+
as: Component = "div",
|
|
451
|
+
className
|
|
452
|
+
}) => {
|
|
453
|
+
const { subscriptions, loading } = useSubscription();
|
|
454
|
+
if (children) {
|
|
455
|
+
return /* @__PURE__ */ jsx3(Fragment2, { children: children({ subscriptions, loading }) });
|
|
456
|
+
}
|
|
457
|
+
const computedClassName = typeof className === "function" ? className({ subscriptions }) : className;
|
|
458
|
+
const activeSubs = subscriptions.filter((sub) => sub.status === "active");
|
|
459
|
+
const latestSub = activeSubs.length > 0 ? activeSubs.reduce((latest, current) => {
|
|
460
|
+
return new Date(current.startDate) > new Date(latest.startDate) ? current : latest;
|
|
461
|
+
}) : null;
|
|
462
|
+
const displayText = loading ? "Loading..." : latestSub ? latestSub.planName : "Free";
|
|
463
|
+
return /* @__PURE__ */ jsx3(
|
|
464
|
+
Component,
|
|
465
|
+
{
|
|
466
|
+
className: computedClassName,
|
|
467
|
+
"data-loading": loading,
|
|
468
|
+
"data-has-subscription": activeSubs.length > 0,
|
|
469
|
+
role: "status",
|
|
470
|
+
"aria-live": "polite",
|
|
471
|
+
"aria-busy": loading,
|
|
472
|
+
"aria-label": loading ? "Loading subscription status" : `Current plan: ${displayText}`,
|
|
473
|
+
children: displayText
|
|
474
|
+
}
|
|
475
|
+
);
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
// src/components/SubscriptionGate.tsx
|
|
479
|
+
import { Fragment as Fragment3, jsx as jsx4 } from "react/jsx-runtime";
|
|
480
|
+
var SubscriptionGate = ({
|
|
481
|
+
requirePlan,
|
|
482
|
+
children
|
|
483
|
+
}) => {
|
|
484
|
+
const { subscriptions, loading, hasPlan } = useSubscription();
|
|
485
|
+
const hasAccess = requirePlan ? hasPlan(requirePlan) : subscriptions.some((sub) => sub.status === "active");
|
|
486
|
+
return /* @__PURE__ */ jsx4(Fragment3, { children: children({
|
|
487
|
+
hasAccess,
|
|
488
|
+
subscriptions,
|
|
489
|
+
loading
|
|
490
|
+
}) });
|
|
491
|
+
};
|
|
241
492
|
export {
|
|
242
493
|
PaymentForm,
|
|
243
|
-
|
|
494
|
+
PlanBadge,
|
|
495
|
+
SolvaPayProvider,
|
|
496
|
+
SubscriptionGate,
|
|
497
|
+
useCheckout,
|
|
498
|
+
useSolvaPay,
|
|
499
|
+
useSubscription
|
|
244
500
|
};
|