@solvapay/react 1.0.0-preview.1
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 +34 -0
- package/dist/index.cjs +272 -0
- package/dist/index.d.cts +84 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +244 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# @solvapay/react
|
|
2
|
+
|
|
3
|
+
Payment components for SolvaPay with Stripe integration.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
```bash
|
|
7
|
+
pnpm add @solvapay/react
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
```tsx
|
|
12
|
+
import { SolvaPayProvider, PaymentForm } from '@solvapay/react';
|
|
13
|
+
|
|
14
|
+
export default function CheckoutPage() {
|
|
15
|
+
return (
|
|
16
|
+
<SolvaPayProvider
|
|
17
|
+
amount={999}
|
|
18
|
+
currency="USD"
|
|
19
|
+
planRef="pln_abc123"
|
|
20
|
+
>
|
|
21
|
+
<PaymentForm
|
|
22
|
+
returnUrl="/checkout/success"
|
|
23
|
+
onSuccess={(paymentIntent) => {
|
|
24
|
+
console.log('Payment successful!', paymentIntent);
|
|
25
|
+
}}
|
|
26
|
+
/>
|
|
27
|
+
</SolvaPayProvider>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Peer deps: react, react-dom
|
|
33
|
+
|
|
34
|
+
More: docs/architecture.md
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.tsx
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
PaymentForm: () => PaymentForm,
|
|
24
|
+
SolvaPayProvider: () => SolvaPayProvider
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/SolvaPayProvider.tsx
|
|
29
|
+
var import_react = require("react");
|
|
30
|
+
var import_react_stripe_js = require("@stripe/react-stripe-js");
|
|
31
|
+
var import_stripe_js = require("@stripe/stripe-js");
|
|
32
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
33
|
+
var SolvaPayProvider = ({
|
|
34
|
+
children,
|
|
35
|
+
amount = 999,
|
|
36
|
+
// Default $9.99
|
|
37
|
+
currency = "USD",
|
|
38
|
+
planRef,
|
|
39
|
+
agentRef,
|
|
40
|
+
apiBaseUrl = "/api/solvapay",
|
|
41
|
+
onPaymentReady
|
|
42
|
+
}) => {
|
|
43
|
+
const [stripePromise, setStripePromise] = (0, import_react.useState)(null);
|
|
44
|
+
const [clientSecret, setClientSecret] = (0, import_react.useState)("");
|
|
45
|
+
const [loading, setLoading] = (0, import_react.useState)(false);
|
|
46
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
47
|
+
const createPayment = async (paymentAmount) => {
|
|
48
|
+
setLoading(true);
|
|
49
|
+
setError(null);
|
|
50
|
+
try {
|
|
51
|
+
console.log("Creating SolvaPay payment intent for amount:", paymentAmount);
|
|
52
|
+
const response = await fetch(`${apiBaseUrl}/create-payment-intent`, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: {
|
|
55
|
+
"Content-Type": "application/json"
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
planRef,
|
|
59
|
+
agentRef,
|
|
60
|
+
amount: paymentAmount,
|
|
61
|
+
currency,
|
|
62
|
+
description: "SolvaPay - Plan Purchase"
|
|
63
|
+
})
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
const errorText = await response.text();
|
|
67
|
+
throw new Error(`Payment intent creation failed: ${response.status} ${errorText}`);
|
|
68
|
+
}
|
|
69
|
+
const result = await response.json();
|
|
70
|
+
if (!result || !result.clientSecret || !result.publishableKey) {
|
|
71
|
+
throw new Error("Invalid response from server: missing required fields");
|
|
72
|
+
}
|
|
73
|
+
console.log("Payment intent received:", {
|
|
74
|
+
hasClientSecret: !!result.clientSecret,
|
|
75
|
+
hasPublishableKey: !!result.publishableKey,
|
|
76
|
+
hasAccountId: !!result.accountId,
|
|
77
|
+
clientSecretLength: result.clientSecret?.length || 0,
|
|
78
|
+
publishableKeyLength: result.publishableKey?.length || 0
|
|
79
|
+
});
|
|
80
|
+
const stripeOptions = result.accountId ? { stripeAccount: result.accountId } : {};
|
|
81
|
+
const stripe = await (0, import_stripe_js.loadStripe)(result.publishableKey, stripeOptions);
|
|
82
|
+
if (!stripe) {
|
|
83
|
+
throw new Error("Failed to initialize Stripe. Please check your configuration.");
|
|
84
|
+
}
|
|
85
|
+
setStripePromise(stripe);
|
|
86
|
+
setClientSecret(result.clientSecret);
|
|
87
|
+
if (onPaymentReady) {
|
|
88
|
+
onPaymentReady(stripe, result.clientSecret);
|
|
89
|
+
}
|
|
90
|
+
} catch (error2) {
|
|
91
|
+
console.error("Failed to create payment:", error2);
|
|
92
|
+
let errorMessage = "Payment creation failed";
|
|
93
|
+
if (error2 instanceof Error) {
|
|
94
|
+
if (error2.message.includes("publishable key")) {
|
|
95
|
+
errorMessage = "Server configuration error: Missing Stripe publishable key";
|
|
96
|
+
} else if (error2.message.includes("client secret")) {
|
|
97
|
+
errorMessage = "Server error: Payment intent creation failed";
|
|
98
|
+
} else if (error2.message.includes("Failed to initialize Stripe")) {
|
|
99
|
+
errorMessage = "Stripe initialization failed. Please check your configuration.";
|
|
100
|
+
} else {
|
|
101
|
+
errorMessage = error2.message;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
setError(errorMessage);
|
|
105
|
+
} finally {
|
|
106
|
+
setLoading(false);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
(0, import_react.useEffect)(() => {
|
|
110
|
+
if (amount && !stripePromise && !loading && !error) {
|
|
111
|
+
createPayment(amount);
|
|
112
|
+
}
|
|
113
|
+
}, [amount]);
|
|
114
|
+
if (error) {
|
|
115
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: {
|
|
116
|
+
padding: "1rem",
|
|
117
|
+
backgroundColor: "#fed7d7",
|
|
118
|
+
color: "#742a2a",
|
|
119
|
+
borderRadius: "0.375rem",
|
|
120
|
+
marginBottom: "1rem"
|
|
121
|
+
}, children: [
|
|
122
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: "Payment Setup Error:" }),
|
|
123
|
+
" ",
|
|
124
|
+
error,
|
|
125
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
126
|
+
"button",
|
|
127
|
+
{
|
|
128
|
+
onClick: () => createPayment(amount),
|
|
129
|
+
style: {
|
|
130
|
+
marginLeft: "1rem",
|
|
131
|
+
padding: "0.5rem 1rem",
|
|
132
|
+
backgroundColor: "#742a2a",
|
|
133
|
+
color: "white",
|
|
134
|
+
border: "none",
|
|
135
|
+
borderRadius: "0.25rem",
|
|
136
|
+
cursor: "pointer"
|
|
137
|
+
},
|
|
138
|
+
children: "Retry"
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
] });
|
|
142
|
+
}
|
|
143
|
+
if (loading) {
|
|
144
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
|
|
145
|
+
padding: "1rem",
|
|
146
|
+
textAlign: "center",
|
|
147
|
+
color: "#4a5568"
|
|
148
|
+
}, children: "Setting up payment..." });
|
|
149
|
+
}
|
|
150
|
+
if (!stripePromise || !clientSecret) {
|
|
151
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
|
|
152
|
+
padding: "1rem",
|
|
153
|
+
textAlign: "center",
|
|
154
|
+
color: "#4a5568"
|
|
155
|
+
}, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
156
|
+
"button",
|
|
157
|
+
{
|
|
158
|
+
onClick: () => createPayment(amount),
|
|
159
|
+
style: {
|
|
160
|
+
padding: "0.75rem 1.5rem",
|
|
161
|
+
backgroundColor: "#3182ce",
|
|
162
|
+
color: "white",
|
|
163
|
+
border: "none",
|
|
164
|
+
borderRadius: "0.375rem",
|
|
165
|
+
cursor: "pointer",
|
|
166
|
+
fontSize: "1rem"
|
|
167
|
+
},
|
|
168
|
+
children: "Initialize Payment"
|
|
169
|
+
}
|
|
170
|
+
) });
|
|
171
|
+
}
|
|
172
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_stripe_js.Elements, { stripe: stripePromise, options: { clientSecret }, children });
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// src/PaymentForm.tsx
|
|
176
|
+
var import_react2 = require("react");
|
|
177
|
+
var import_react_stripe_js2 = require("@stripe/react-stripe-js");
|
|
178
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
179
|
+
var PaymentForm = ({
|
|
180
|
+
onSuccess,
|
|
181
|
+
onError,
|
|
182
|
+
returnUrl,
|
|
183
|
+
submitButtonText = "Pay Now",
|
|
184
|
+
className
|
|
185
|
+
}) => {
|
|
186
|
+
const stripe = (0, import_react_stripe_js2.useStripe)();
|
|
187
|
+
const elements = (0, import_react_stripe_js2.useElements)();
|
|
188
|
+
const [isProcessing, setIsProcessing] = (0, import_react2.useState)(false);
|
|
189
|
+
const [message, setMessage] = (0, import_react2.useState)(null);
|
|
190
|
+
const handleSubmit = async (event) => {
|
|
191
|
+
event.preventDefault();
|
|
192
|
+
if (!stripe || !elements) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
setIsProcessing(true);
|
|
196
|
+
setMessage(null);
|
|
197
|
+
try {
|
|
198
|
+
const { error, paymentIntent } = await stripe.confirmPayment({
|
|
199
|
+
elements,
|
|
200
|
+
confirmParams: {
|
|
201
|
+
return_url: returnUrl || `${window.location.origin}/checkout/complete`
|
|
202
|
+
},
|
|
203
|
+
redirect: "if_required"
|
|
204
|
+
// Only redirect if required by payment method
|
|
205
|
+
});
|
|
206
|
+
if (error) {
|
|
207
|
+
const errorMessage = error.message || "An unexpected error occurred.";
|
|
208
|
+
setMessage(errorMessage);
|
|
209
|
+
if (onError) {
|
|
210
|
+
onError(new Error(errorMessage));
|
|
211
|
+
}
|
|
212
|
+
} else if (paymentIntent && paymentIntent.status === "succeeded") {
|
|
213
|
+
setMessage("Payment successful!");
|
|
214
|
+
if (onSuccess) {
|
|
215
|
+
onSuccess(paymentIntent);
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
setMessage(`Payment status: ${paymentIntent?.status || "processing"}`);
|
|
219
|
+
}
|
|
220
|
+
} catch (err) {
|
|
221
|
+
const error = err instanceof Error ? err : new Error("Unknown error occurred");
|
|
222
|
+
setMessage(error.message);
|
|
223
|
+
if (onError) {
|
|
224
|
+
onError(error);
|
|
225
|
+
}
|
|
226
|
+
} finally {
|
|
227
|
+
setIsProcessing(false);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("form", { onSubmit: handleSubmit, className, children: [
|
|
231
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_stripe_js2.PaymentElement, {}),
|
|
232
|
+
message && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
233
|
+
"div",
|
|
234
|
+
{
|
|
235
|
+
style: {
|
|
236
|
+
marginTop: "1rem",
|
|
237
|
+
padding: "0.75rem",
|
|
238
|
+
borderRadius: "0.375rem",
|
|
239
|
+
backgroundColor: message.includes("successful") ? "#d1fae5" : "#fee2e2",
|
|
240
|
+
color: message.includes("successful") ? "#065f46" : "#7f1d1d"
|
|
241
|
+
},
|
|
242
|
+
children: message
|
|
243
|
+
}
|
|
244
|
+
),
|
|
245
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
246
|
+
"button",
|
|
247
|
+
{
|
|
248
|
+
type: "submit",
|
|
249
|
+
disabled: !stripe || isProcessing,
|
|
250
|
+
style: {
|
|
251
|
+
marginTop: "1.5rem",
|
|
252
|
+
width: "100%",
|
|
253
|
+
padding: "0.75rem 1.5rem",
|
|
254
|
+
backgroundColor: isProcessing || !stripe ? "#9ca3af" : "#3b82f6",
|
|
255
|
+
color: "white",
|
|
256
|
+
border: "none",
|
|
257
|
+
borderRadius: "0.375rem",
|
|
258
|
+
fontSize: "1rem",
|
|
259
|
+
fontWeight: "500",
|
|
260
|
+
cursor: isProcessing || !stripe ? "not-allowed" : "pointer",
|
|
261
|
+
transition: "background-color 0.2s"
|
|
262
|
+
},
|
|
263
|
+
children: isProcessing ? "Processing..." : submitButtonText
|
|
264
|
+
}
|
|
265
|
+
)
|
|
266
|
+
] });
|
|
267
|
+
};
|
|
268
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
269
|
+
0 && (module.exports = {
|
|
270
|
+
PaymentForm,
|
|
271
|
+
SolvaPayProvider
|
|
272
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Stripe } from '@stripe/stripe-js';
|
|
3
|
+
|
|
4
|
+
interface SolvaPayProviderProps {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
amount?: number;
|
|
7
|
+
currency?: string;
|
|
8
|
+
planRef: string;
|
|
9
|
+
agentRef: string;
|
|
10
|
+
apiBaseUrl?: string;
|
|
11
|
+
onPaymentReady?: (stripe: Stripe | null, clientSecret: string) => void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* SolvaPay Payment Provider Component
|
|
15
|
+
*
|
|
16
|
+
* Wraps children with Stripe Elements context after initializing a payment intent
|
|
17
|
+
* via the backend API. This component handles the secure payment flow:
|
|
18
|
+
*
|
|
19
|
+
* 1. Calls backend API route to create payment intent (backend uses secret key)
|
|
20
|
+
* 2. Backend returns clientSecret and publishableKey from Stripe
|
|
21
|
+
* 3. Initializes Stripe.js with publishableKey
|
|
22
|
+
* 4. Wraps children in Stripe Elements provider
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* // In your Next.js page
|
|
27
|
+
* import { SolvaPayProvider, PaymentForm } from '@solvapay/react';
|
|
28
|
+
*
|
|
29
|
+
* export default function CheckoutPage() {
|
|
30
|
+
* return (
|
|
31
|
+
* <SolvaPayProvider
|
|
32
|
+
* amount={999}
|
|
33
|
+
* currency="USD"
|
|
34
|
+
* planRef="pln_abc123"
|
|
35
|
+
* agentRef="agt_xyz789"
|
|
36
|
+
* onPaymentReady={(stripe, clientSecret) => {
|
|
37
|
+
* console.log('Payment ready!');
|
|
38
|
+
* }}
|
|
39
|
+
* >
|
|
40
|
+
* <PaymentForm />
|
|
41
|
+
* </SolvaPayProvider>
|
|
42
|
+
* );
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
declare const SolvaPayProvider: React.FC<SolvaPayProviderProps>;
|
|
47
|
+
|
|
48
|
+
interface PaymentFormProps {
|
|
49
|
+
onSuccess?: (paymentIntent: any) => void;
|
|
50
|
+
onError?: (error: Error) => void;
|
|
51
|
+
returnUrl?: string;
|
|
52
|
+
submitButtonText?: string;
|
|
53
|
+
className?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* SolvaPay Payment Form Component
|
|
57
|
+
*
|
|
58
|
+
* Renders a payment form using Stripe's PaymentElement for secure card input.
|
|
59
|
+
* This component must be wrapped in a SolvaPayProvider to work properly.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```tsx
|
|
63
|
+
* import { SolvaPayProvider, PaymentForm } from '@solvapay/react';
|
|
64
|
+
*
|
|
65
|
+
* export default function CheckoutPage() {
|
|
66
|
+
* return (
|
|
67
|
+
* <SolvaPayProvider amount={999} planRef="pln_abc123">
|
|
68
|
+
* <PaymentForm
|
|
69
|
+
* returnUrl={`${window.location.origin}/checkout/success`}
|
|
70
|
+
* onSuccess={(paymentIntent) => {
|
|
71
|
+
* console.log('Payment successful!', paymentIntent);
|
|
72
|
+
* }}
|
|
73
|
+
* onError={(error) => {
|
|
74
|
+
* console.error('Payment failed:', error);
|
|
75
|
+
* }}
|
|
76
|
+
* />
|
|
77
|
+
* </SolvaPayProvider>
|
|
78
|
+
* );
|
|
79
|
+
* }
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
declare const PaymentForm: React.FC<PaymentFormProps>;
|
|
83
|
+
|
|
84
|
+
export { PaymentForm, type PaymentFormProps, SolvaPayProvider, type SolvaPayProviderProps };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Stripe } from '@stripe/stripe-js';
|
|
3
|
+
|
|
4
|
+
interface SolvaPayProviderProps {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
amount?: number;
|
|
7
|
+
currency?: string;
|
|
8
|
+
planRef: string;
|
|
9
|
+
agentRef: string;
|
|
10
|
+
apiBaseUrl?: string;
|
|
11
|
+
onPaymentReady?: (stripe: Stripe | null, clientSecret: string) => void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* SolvaPay Payment Provider Component
|
|
15
|
+
*
|
|
16
|
+
* Wraps children with Stripe Elements context after initializing a payment intent
|
|
17
|
+
* via the backend API. This component handles the secure payment flow:
|
|
18
|
+
*
|
|
19
|
+
* 1. Calls backend API route to create payment intent (backend uses secret key)
|
|
20
|
+
* 2. Backend returns clientSecret and publishableKey from Stripe
|
|
21
|
+
* 3. Initializes Stripe.js with publishableKey
|
|
22
|
+
* 4. Wraps children in Stripe Elements provider
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* // In your Next.js page
|
|
27
|
+
* import { SolvaPayProvider, PaymentForm } from '@solvapay/react';
|
|
28
|
+
*
|
|
29
|
+
* export default function CheckoutPage() {
|
|
30
|
+
* return (
|
|
31
|
+
* <SolvaPayProvider
|
|
32
|
+
* amount={999}
|
|
33
|
+
* currency="USD"
|
|
34
|
+
* planRef="pln_abc123"
|
|
35
|
+
* agentRef="agt_xyz789"
|
|
36
|
+
* onPaymentReady={(stripe, clientSecret) => {
|
|
37
|
+
* console.log('Payment ready!');
|
|
38
|
+
* }}
|
|
39
|
+
* >
|
|
40
|
+
* <PaymentForm />
|
|
41
|
+
* </SolvaPayProvider>
|
|
42
|
+
* );
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
declare const SolvaPayProvider: React.FC<SolvaPayProviderProps>;
|
|
47
|
+
|
|
48
|
+
interface PaymentFormProps {
|
|
49
|
+
onSuccess?: (paymentIntent: any) => void;
|
|
50
|
+
onError?: (error: Error) => void;
|
|
51
|
+
returnUrl?: string;
|
|
52
|
+
submitButtonText?: string;
|
|
53
|
+
className?: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* SolvaPay Payment Form Component
|
|
57
|
+
*
|
|
58
|
+
* Renders a payment form using Stripe's PaymentElement for secure card input.
|
|
59
|
+
* This component must be wrapped in a SolvaPayProvider to work properly.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```tsx
|
|
63
|
+
* import { SolvaPayProvider, PaymentForm } from '@solvapay/react';
|
|
64
|
+
*
|
|
65
|
+
* export default function CheckoutPage() {
|
|
66
|
+
* return (
|
|
67
|
+
* <SolvaPayProvider amount={999} planRef="pln_abc123">
|
|
68
|
+
* <PaymentForm
|
|
69
|
+
* returnUrl={`${window.location.origin}/checkout/success`}
|
|
70
|
+
* onSuccess={(paymentIntent) => {
|
|
71
|
+
* console.log('Payment successful!', paymentIntent);
|
|
72
|
+
* }}
|
|
73
|
+
* onError={(error) => {
|
|
74
|
+
* console.error('Payment failed:', error);
|
|
75
|
+
* }}
|
|
76
|
+
* />
|
|
77
|
+
* </SolvaPayProvider>
|
|
78
|
+
* );
|
|
79
|
+
* }
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
declare const PaymentForm: React.FC<PaymentFormProps>;
|
|
83
|
+
|
|
84
|
+
export { PaymentForm, type PaymentFormProps, SolvaPayProvider, type SolvaPayProviderProps };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
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";
|
|
6
|
+
var SolvaPayProvider = ({
|
|
7
|
+
children,
|
|
8
|
+
amount = 999,
|
|
9
|
+
// Default $9.99
|
|
10
|
+
currency = "USD",
|
|
11
|
+
planRef,
|
|
12
|
+
agentRef,
|
|
13
|
+
apiBaseUrl = "/api/solvapay",
|
|
14
|
+
onPaymentReady
|
|
15
|
+
}) => {
|
|
16
|
+
const [stripePromise, setStripePromise] = useState(null);
|
|
17
|
+
const [clientSecret, setClientSecret] = useState("");
|
|
18
|
+
const [loading, setLoading] = useState(false);
|
|
19
|
+
const [error, setError] = useState(null);
|
|
20
|
+
const createPayment = async (paymentAmount) => {
|
|
21
|
+
setLoading(true);
|
|
22
|
+
setError(null);
|
|
23
|
+
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
|
+
});
|
|
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
|
+
}
|
|
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.");
|
|
57
|
+
}
|
|
58
|
+
setStripePromise(stripe);
|
|
59
|
+
setClientSecret(result.clientSecret);
|
|
60
|
+
if (onPaymentReady) {
|
|
61
|
+
onPaymentReady(stripe, result.clientSecret);
|
|
62
|
+
}
|
|
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.";
|
|
73
|
+
} else {
|
|
74
|
+
errorMessage = error2.message;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
setError(errorMessage);
|
|
78
|
+
} finally {
|
|
79
|
+
setLoading(false);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
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
|
+
};
|
|
147
|
+
|
|
148
|
+
// 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 = ({
|
|
153
|
+
onSuccess,
|
|
154
|
+
onError,
|
|
155
|
+
returnUrl,
|
|
156
|
+
submitButtonText = "Pay Now",
|
|
157
|
+
className
|
|
158
|
+
}) => {
|
|
159
|
+
const stripe = useStripe();
|
|
160
|
+
const elements = useElements();
|
|
161
|
+
const [isProcessing, setIsProcessing] = useState2(false);
|
|
162
|
+
const [message, setMessage] = useState2(null);
|
|
163
|
+
const handleSubmit = async (event) => {
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
if (!stripe || !elements) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
setIsProcessing(true);
|
|
169
|
+
setMessage(null);
|
|
170
|
+
try {
|
|
171
|
+
const { error, paymentIntent } = await stripe.confirmPayment({
|
|
172
|
+
elements,
|
|
173
|
+
confirmParams: {
|
|
174
|
+
return_url: returnUrl || `${window.location.origin}/checkout/complete`
|
|
175
|
+
},
|
|
176
|
+
redirect: "if_required"
|
|
177
|
+
// Only redirect if required by payment method
|
|
178
|
+
});
|
|
179
|
+
if (error) {
|
|
180
|
+
const errorMessage = error.message || "An unexpected error occurred.";
|
|
181
|
+
setMessage(errorMessage);
|
|
182
|
+
if (onError) {
|
|
183
|
+
onError(new Error(errorMessage));
|
|
184
|
+
}
|
|
185
|
+
} else if (paymentIntent && paymentIntent.status === "succeeded") {
|
|
186
|
+
setMessage("Payment successful!");
|
|
187
|
+
if (onSuccess) {
|
|
188
|
+
onSuccess(paymentIntent);
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
setMessage(`Payment status: ${paymentIntent?.status || "processing"}`);
|
|
192
|
+
}
|
|
193
|
+
} catch (err) {
|
|
194
|
+
const error = err instanceof Error ? err : new Error("Unknown error occurred");
|
|
195
|
+
setMessage(error.message);
|
|
196
|
+
if (onError) {
|
|
197
|
+
onError(error);
|
|
198
|
+
}
|
|
199
|
+
} finally {
|
|
200
|
+
setIsProcessing(false);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
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"
|
|
214
|
+
},
|
|
215
|
+
children: message
|
|
216
|
+
}
|
|
217
|
+
),
|
|
218
|
+
/* @__PURE__ */ jsx2(
|
|
219
|
+
"button",
|
|
220
|
+
{
|
|
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
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
] });
|
|
240
|
+
};
|
|
241
|
+
export {
|
|
242
|
+
PaymentForm,
|
|
243
|
+
SolvaPayProvider
|
|
244
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@solvapay/react",
|
|
3
|
+
"version": "1.0.0-preview.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/solvapay/solvapay-sdk.git",
|
|
18
|
+
"directory": "packages/react"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"react": "^18.2.0 || ^19.0.0",
|
|
22
|
+
"react-dom": "^18.2.0 || ^19.0.0"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@stripe/react-stripe-js": "^2.9.0",
|
|
26
|
+
"@stripe/stripe-js": "^4.8.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/react": "^18.2.74",
|
|
30
|
+
"@types/react-dom": "^18.2.25",
|
|
31
|
+
"typescript": "^5.5.4",
|
|
32
|
+
"vitest": "^2.0.5",
|
|
33
|
+
"@solvapay/test-utils": "0.0.0"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup src/index.tsx --format esm,cjs --dts --tsconfig tsconfig.build.json",
|
|
37
|
+
"test": "vitest run || exit 0",
|
|
38
|
+
"test:unit": "vitest run || exit 0",
|
|
39
|
+
"test:watch": "vitest"
|
|
40
|
+
}
|
|
41
|
+
}
|