@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/dist/index.js CHANGED
@@ -1,177 +1,253 @@
1
1
  // src/SolvaPayProvider.tsx
2
- import { useState, useEffect } from "react";
3
- import { Elements } from "@stripe/react-stripe-js";
4
- import { loadStripe } from "@stripe/stripe-js";
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
- children,
8
- amount = 999,
9
- // Default $9.99
10
- currency = "USD",
11
- planRef,
12
- agentRef,
13
- apiBaseUrl = "/api/solvapay",
14
- onPaymentReady
6
+ createPayment,
7
+ checkSubscription,
8
+ customerRef,
9
+ onCustomerRefUpdate,
10
+ children
15
11
  }) => {
16
- const [stripePromise, setStripePromise] = useState(null);
17
- const [clientSecret, setClientSecret] = useState("");
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 [error, setError] = useState(null);
20
- const createPayment = async (paymentAmount) => {
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
- console.log("Creating SolvaPay payment intent for amount:", paymentAmount);
25
- const response = await fetch(`${apiBaseUrl}/create-payment-intent`, {
26
- method: "POST",
27
- headers: {
28
- "Content-Type": "application/json"
29
- },
30
- body: JSON.stringify({
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
- if (!response.ok) {
39
- const errorText = await response.text();
40
- throw new Error(`Payment intent creation failed: ${response.status} ${errorText}`);
41
- }
42
- const result = await response.json();
43
- if (!result || !result.clientSecret || !result.publishableKey) {
44
- throw new Error("Invalid response from server: missing required fields");
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
- console.log("Payment intent received:", {
47
- hasClientSecret: !!result.clientSecret,
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
- setStripePromise(stripe);
59
- setClientSecret(result.clientSecret);
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
- } catch (error2) {
64
- console.error("Failed to create payment:", error2);
65
- let errorMessage = "Payment creation failed";
66
- if (error2 instanceof Error) {
67
- if (error2.message.includes("publishable key")) {
68
- errorMessage = "Server configuration error: Missing Stripe publishable key";
69
- } else if (error2.message.includes("client secret")) {
70
- errorMessage = "Server error: Payment intent creation failed";
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
- errorMessage = error2.message;
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
- setError(errorMessage);
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
- useEffect(() => {
83
- if (amount && !stripePromise && !loading && !error) {
84
- createPayment(amount);
85
- }
86
- }, [amount]);
87
- if (error) {
88
- return /* @__PURE__ */ jsxs("div", { style: {
89
- padding: "1rem",
90
- backgroundColor: "#fed7d7",
91
- color: "#742a2a",
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 { useState as useState2 } from "react";
150
- import { useStripe, useElements, PaymentElement } from "@stripe/react-stripe-js";
151
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
152
- var PaymentForm = ({
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
- className
227
+ buttonClassName
158
228
  }) => {
159
229
  const stripe = useStripe();
160
230
  const elements = useElements();
161
- const [isProcessing, setIsProcessing] = useState2(false);
162
- const [message, setMessage] = useState2(null);
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: returnUrl || `${window.location.origin}/checkout/complete`
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?.status || "processing"}`);
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
- return /* @__PURE__ */ jsxs2("form", { onSubmit: handleSubmit, className, children: [
204
- /* @__PURE__ */ jsx2(PaymentElement, {}),
205
- message && /* @__PURE__ */ jsx2(
206
- "div",
207
- {
208
- style: {
209
- marginTop: "1rem",
210
- padding: "0.75rem",
211
- borderRadius: "0.375rem",
212
- backgroundColor: message.includes("successful") ? "#d1fae5" : "#fee2e2",
213
- color: message.includes("successful") ? "#065f46" : "#7f1d1d"
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
- children: message
216
- }
217
- ),
218
- /* @__PURE__ */ jsx2(
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
- type: "submit",
222
- disabled: !stripe || isProcessing,
223
- style: {
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
- SolvaPayProvider
494
+ PlanBadge,
495
+ SolvaPayProvider,
496
+ SubscriptionGate,
497
+ useCheckout,
498
+ useSolvaPay,
499
+ useSubscription
244
500
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solvapay/react",
3
- "version": "1.0.0-preview.4",
3
+ "version": "1.0.0-preview.6",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",