@micro-cms/crypto-payments 1.0.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/dist/index.d.mts +54 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +247 -0
- package/dist/index.mjs +220 -0
- package/package.json +31 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import react__default from 'react';
|
|
3
|
+
import { PaymentIntent, PaymentVerification } from '@micro-cms/types';
|
|
4
|
+
|
|
5
|
+
interface SolanaWindow extends Window {
|
|
6
|
+
solana?: {
|
|
7
|
+
isPhantom?: boolean;
|
|
8
|
+
connect: (options?: {
|
|
9
|
+
onlyIfTrusted?: boolean;
|
|
10
|
+
}) => Promise<{
|
|
11
|
+
publicKey: {
|
|
12
|
+
toString: () => string;
|
|
13
|
+
};
|
|
14
|
+
}>;
|
|
15
|
+
signTransaction: (transaction: any) => Promise<any>;
|
|
16
|
+
request: (request: {
|
|
17
|
+
method: string;
|
|
18
|
+
params?: any;
|
|
19
|
+
}) => Promise<any>;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
declare const useSolanaWallet: () => {
|
|
23
|
+
isAvailable: boolean;
|
|
24
|
+
connect: () => Promise<string>;
|
|
25
|
+
sendPayment: (intent: PaymentIntent) => Promise<string>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
declare const PaymentWidget: react__default.FC<PaymentWidgetProps>;
|
|
29
|
+
|
|
30
|
+
interface PaymentWidgetProps {
|
|
31
|
+
orderId: string;
|
|
32
|
+
amount?: number;
|
|
33
|
+
currency?: string;
|
|
34
|
+
onSuccess?: (verification: PaymentVerification) => void;
|
|
35
|
+
onError?: (error: Error) => void;
|
|
36
|
+
endpoints?: {
|
|
37
|
+
initiate?: string;
|
|
38
|
+
verify?: string;
|
|
39
|
+
};
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
type PaymentStatus = 'idle' | 'connecting' | 'initiating' | 'pending_signature' | 'verifying' | 'success' | 'error';
|
|
43
|
+
declare const usePayment: (props: PaymentWidgetProps) => {
|
|
44
|
+
status: PaymentStatus;
|
|
45
|
+
intent: PaymentIntent | null;
|
|
46
|
+
error: string | null;
|
|
47
|
+
initiate: () => Promise<void>;
|
|
48
|
+
verify: (txHash: string) => Promise<void>;
|
|
49
|
+
setStatus: react.Dispatch<react.SetStateAction<PaymentStatus>>;
|
|
50
|
+
isSolanaAvailable: boolean;
|
|
51
|
+
handleSolanaPay: () => Promise<void>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export { type PaymentStatus, PaymentWidget, type PaymentWidgetProps, type SolanaWindow, usePayment, useSolanaWallet };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import react__default from 'react';
|
|
3
|
+
import { PaymentIntent, PaymentVerification } from '@micro-cms/types';
|
|
4
|
+
|
|
5
|
+
interface SolanaWindow extends Window {
|
|
6
|
+
solana?: {
|
|
7
|
+
isPhantom?: boolean;
|
|
8
|
+
connect: (options?: {
|
|
9
|
+
onlyIfTrusted?: boolean;
|
|
10
|
+
}) => Promise<{
|
|
11
|
+
publicKey: {
|
|
12
|
+
toString: () => string;
|
|
13
|
+
};
|
|
14
|
+
}>;
|
|
15
|
+
signTransaction: (transaction: any) => Promise<any>;
|
|
16
|
+
request: (request: {
|
|
17
|
+
method: string;
|
|
18
|
+
params?: any;
|
|
19
|
+
}) => Promise<any>;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
declare const useSolanaWallet: () => {
|
|
23
|
+
isAvailable: boolean;
|
|
24
|
+
connect: () => Promise<string>;
|
|
25
|
+
sendPayment: (intent: PaymentIntent) => Promise<string>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
declare const PaymentWidget: react__default.FC<PaymentWidgetProps>;
|
|
29
|
+
|
|
30
|
+
interface PaymentWidgetProps {
|
|
31
|
+
orderId: string;
|
|
32
|
+
amount?: number;
|
|
33
|
+
currency?: string;
|
|
34
|
+
onSuccess?: (verification: PaymentVerification) => void;
|
|
35
|
+
onError?: (error: Error) => void;
|
|
36
|
+
endpoints?: {
|
|
37
|
+
initiate?: string;
|
|
38
|
+
verify?: string;
|
|
39
|
+
};
|
|
40
|
+
className?: string;
|
|
41
|
+
}
|
|
42
|
+
type PaymentStatus = 'idle' | 'connecting' | 'initiating' | 'pending_signature' | 'verifying' | 'success' | 'error';
|
|
43
|
+
declare const usePayment: (props: PaymentWidgetProps) => {
|
|
44
|
+
status: PaymentStatus;
|
|
45
|
+
intent: PaymentIntent | null;
|
|
46
|
+
error: string | null;
|
|
47
|
+
initiate: () => Promise<void>;
|
|
48
|
+
verify: (txHash: string) => Promise<void>;
|
|
49
|
+
setStatus: react.Dispatch<react.SetStateAction<PaymentStatus>>;
|
|
50
|
+
isSolanaAvailable: boolean;
|
|
51
|
+
handleSolanaPay: () => Promise<void>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export { type PaymentStatus, PaymentWidget, type PaymentWidgetProps, type SolanaWindow, usePayment, useSolanaWallet };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
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.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
PaymentWidget: () => PaymentWidget,
|
|
24
|
+
usePayment: () => usePayment,
|
|
25
|
+
useSolanaWallet: () => useSolanaWallet
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(index_exports);
|
|
28
|
+
var import_react = require("react");
|
|
29
|
+
|
|
30
|
+
// src/providers/SolanaProvider.ts
|
|
31
|
+
var useSolanaWallet = () => {
|
|
32
|
+
const getProvider = () => {
|
|
33
|
+
if ("solana" in window) {
|
|
34
|
+
const anyWindow = window;
|
|
35
|
+
if (anyWindow.solana?.isPhantom) {
|
|
36
|
+
return anyWindow.solana;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return void 0;
|
|
40
|
+
};
|
|
41
|
+
const connect = async () => {
|
|
42
|
+
const provider = getProvider();
|
|
43
|
+
if (provider) {
|
|
44
|
+
try {
|
|
45
|
+
const resp = await provider.connect();
|
|
46
|
+
return resp.publicKey.toString();
|
|
47
|
+
} catch (err) {
|
|
48
|
+
throw new Error("User rejected the connection");
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
throw new Error("Phantom wallet not found");
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const sendPayment = async (intent) => {
|
|
55
|
+
const provider = getProvider();
|
|
56
|
+
if (!provider) throw new Error("Wallet not connected");
|
|
57
|
+
console.log(`Simulating Solana payment to ${intent.paymentAddress} for ${intent.amount} SOL`);
|
|
58
|
+
return "simulated_solana_signature_" + Math.random().toString(36).slice(2);
|
|
59
|
+
};
|
|
60
|
+
return { isAvailable: !!getProvider(), connect, sendPayment };
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/PaymentWidget.tsx
|
|
64
|
+
var import_lucide_react = require("lucide-react");
|
|
65
|
+
var import_clsx = require("clsx");
|
|
66
|
+
var import_tailwind_merge = require("tailwind-merge");
|
|
67
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
68
|
+
function cn(...inputs) {
|
|
69
|
+
return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs));
|
|
70
|
+
}
|
|
71
|
+
var PaymentWidget = ({
|
|
72
|
+
className,
|
|
73
|
+
...props
|
|
74
|
+
}) => {
|
|
75
|
+
const { status, intent, error, initiate, verify, setStatus, isSolanaAvailable, handleSolanaPay } = usePayment(props);
|
|
76
|
+
if (status === "success") {
|
|
77
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cn("p-6 text-center bg-green-50 rounded-xl border border-green-200", className), children: [
|
|
78
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.CheckCircle, { className: "mx-auto w-12 h-12 text-green-500 mb-4" }),
|
|
79
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "text-lg font-semibold text-green-900", children: "Payment Successful" }),
|
|
80
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-green-700 mt-1", children: "Your order has been confirmed." })
|
|
81
|
+
] });
|
|
82
|
+
}
|
|
83
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cn("bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden", className), children: [
|
|
84
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "p-6", children: [
|
|
85
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center gap-3 mb-6", children: [
|
|
86
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "p-2 bg-indigo-50 rounded-lg", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Wallet, { className: "w-5 h-5 text-indigo-600" }) }),
|
|
87
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
88
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "font-semibold text-slate-900", children: "Crypto Payment" }),
|
|
89
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-sm text-slate-500", children: "Pay securely using your wallet" })
|
|
90
|
+
] })
|
|
91
|
+
] }),
|
|
92
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mb-4 p-3 bg-red-50 border border-red-100 rounded-lg flex gap-3 text-red-700 text-sm", children: [
|
|
93
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.AlertCircle, { className: "w-4 h-4 shrink-0" }),
|
|
94
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: error })
|
|
95
|
+
] }),
|
|
96
|
+
status === "idle" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
97
|
+
"button",
|
|
98
|
+
{
|
|
99
|
+
onClick: initiate,
|
|
100
|
+
className: "w-full py-3 px-4 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-xl transition-colors flex items-center justify-center gap-2",
|
|
101
|
+
children: "Start Payment"
|
|
102
|
+
}
|
|
103
|
+
),
|
|
104
|
+
(status === "connecting" || status === "initiating" || status === "verifying") && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "py-8 text-center", children: [
|
|
105
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Loader2, { className: "w-8 h-8 text-indigo-600 animate-spin mx-auto mb-3" }),
|
|
106
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-slate-600 font-medium", children: status === "connecting" ? "Connecting wallet..." : status === "initiating" ? "Preparing transaction..." : "Verifying on-chain..." })
|
|
107
|
+
] }),
|
|
108
|
+
status === "pending_signature" && intent && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-4", children: [
|
|
109
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "p-4 bg-slate-50 rounded-xl space-y-2", children: [
|
|
110
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex justify-between text-sm", children: [
|
|
111
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-slate-500", children: "Amount to pay" }),
|
|
112
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "font-mono font-medium text-slate-900", children: [
|
|
113
|
+
intent.amount,
|
|
114
|
+
" ",
|
|
115
|
+
intent.currency
|
|
116
|
+
] })
|
|
117
|
+
] }),
|
|
118
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex justify-between text-sm", children: [
|
|
119
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-slate-500", children: "Network" }),
|
|
120
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-slate-900 font-medium", children: intent.network })
|
|
121
|
+
] })
|
|
122
|
+
] }),
|
|
123
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "space-y-2", children: [
|
|
124
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "text-xs text-slate-500 uppercase font-bold tracking-wider", children: "Destination Address" }),
|
|
125
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "p-3 bg-slate-100 rounded-lg font-mono text-xs break-all text-slate-600 border border-slate-200", children: intent.paymentAddress })
|
|
126
|
+
] }),
|
|
127
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-col gap-2", children: intent.network.toLowerCase().includes("solana") && isSolanaAvailable ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
128
|
+
"button",
|
|
129
|
+
{
|
|
130
|
+
onClick: handleSolanaPay,
|
|
131
|
+
className: "w-full py-3 px-4 bg-[#512da8] hover:bg-[#4527a0] text-white font-medium rounded-xl transition-colors flex items-center justify-center gap-2",
|
|
132
|
+
children: [
|
|
133
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Cpu, { className: "w-4 h-4" }),
|
|
134
|
+
"Pay with Phantom"
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
138
|
+
"button",
|
|
139
|
+
{
|
|
140
|
+
onClick: () => {
|
|
141
|
+
setStatus("verifying");
|
|
142
|
+
setTimeout(() => verify("0x_mock_transaction_hash_generic"), 2e3);
|
|
143
|
+
},
|
|
144
|
+
className: "w-full py-3 px-4 bg-slate-900 hover:bg-black text-white font-medium rounded-xl transition-colors flex items-center justify-center gap-2",
|
|
145
|
+
children: "Confirm in Wallet"
|
|
146
|
+
}
|
|
147
|
+
) })
|
|
148
|
+
] })
|
|
149
|
+
] }),
|
|
150
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "bg-slate-50 px-6 py-3 border-t border-slate-100 flex justify-between items-center", children: [
|
|
151
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "text-xs text-slate-400", children: "Powered by Micro-CMS Crypto" }),
|
|
152
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("a", { href: "#", className: "text-xs text-indigo-600 hover:text-indigo-700 font-medium flex items-center gap-1", children: [
|
|
153
|
+
"Help ",
|
|
154
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ExternalLink, { className: "w-3 h-3" })
|
|
155
|
+
] })
|
|
156
|
+
] })
|
|
157
|
+
] });
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// src/index.ts
|
|
161
|
+
var usePayment = (props) => {
|
|
162
|
+
const [status, setStatus] = (0, import_react.useState)("idle");
|
|
163
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
164
|
+
const [intent, setIntent] = (0, import_react.useState)(null);
|
|
165
|
+
const initiate = (0, import_react.useCallback)(async () => {
|
|
166
|
+
try {
|
|
167
|
+
setStatus("initiating");
|
|
168
|
+
setError(null);
|
|
169
|
+
const response = await fetch(props.endpoints?.initiate || "/api/orders/initiate", {
|
|
170
|
+
method: "POST",
|
|
171
|
+
headers: { "Content-Type": "application/json" },
|
|
172
|
+
body: JSON.stringify({
|
|
173
|
+
orderId: props.orderId,
|
|
174
|
+
amount: props.amount,
|
|
175
|
+
currency: props.currency
|
|
176
|
+
})
|
|
177
|
+
});
|
|
178
|
+
if (!response.ok) {
|
|
179
|
+
const errorData = await response.json();
|
|
180
|
+
throw new Error(errorData.error || "Failed to initiate payment intent");
|
|
181
|
+
}
|
|
182
|
+
const data = await response.json();
|
|
183
|
+
setIntent(data);
|
|
184
|
+
setStatus("pending_signature");
|
|
185
|
+
} catch (err) {
|
|
186
|
+
setError(err.message);
|
|
187
|
+
setStatus("error");
|
|
188
|
+
props.onError?.(err);
|
|
189
|
+
}
|
|
190
|
+
}, [props.endpoints?.initiate, props.orderId, props.amount, props.currency, props.onError]);
|
|
191
|
+
const verify = (0, import_react.useCallback)(async (txHash) => {
|
|
192
|
+
try {
|
|
193
|
+
setStatus("verifying");
|
|
194
|
+
const response = await fetch(props.endpoints?.verify || "/api/orders/verify-payment", {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: { "Content-Type": "application/json" },
|
|
197
|
+
body: JSON.stringify({
|
|
198
|
+
orderId: props.orderId,
|
|
199
|
+
transactionHash: txHash
|
|
200
|
+
})
|
|
201
|
+
});
|
|
202
|
+
if (!response.ok) {
|
|
203
|
+
const errorData = await response.json();
|
|
204
|
+
throw new Error(errorData.error || "Payment verification failed");
|
|
205
|
+
}
|
|
206
|
+
const data = await response.json();
|
|
207
|
+
if (data.status === "confirmed") {
|
|
208
|
+
setStatus("success");
|
|
209
|
+
props.onSuccess?.(data);
|
|
210
|
+
} else {
|
|
211
|
+
throw new Error("Payment not confirmed yet, status: " + data.status);
|
|
212
|
+
}
|
|
213
|
+
} catch (err) {
|
|
214
|
+
setError(err.message);
|
|
215
|
+
setStatus("error");
|
|
216
|
+
props.onError?.(err);
|
|
217
|
+
}
|
|
218
|
+
}, [props.endpoints?.verify, props.orderId, props.onSuccess, props.onError]);
|
|
219
|
+
const { isAvailable: isSolanaAvailable, connect: connectSolana, sendPayment: sendSolanaPayment } = useSolanaWallet();
|
|
220
|
+
const handleSolanaPay = async () => {
|
|
221
|
+
try {
|
|
222
|
+
if (!intent) {
|
|
223
|
+
setError("Payment intent not established.");
|
|
224
|
+
setStatus("error");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
setStatus("connecting");
|
|
228
|
+
const publicKey = await connectSolana();
|
|
229
|
+
console.log("Connected to Solana with public key:", publicKey);
|
|
230
|
+
setStatus("pending_signature");
|
|
231
|
+
const signature = await sendSolanaPayment(intent);
|
|
232
|
+
setStatus("verifying");
|
|
233
|
+
await verify(signature);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
setError(err.message || "Failed to connect to Phantom or sign transaction.");
|
|
236
|
+
setStatus("error");
|
|
237
|
+
props.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
return { status, intent, error, initiate, verify, setStatus, isSolanaAvailable, handleSolanaPay };
|
|
241
|
+
};
|
|
242
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
243
|
+
0 && (module.exports = {
|
|
244
|
+
PaymentWidget,
|
|
245
|
+
usePayment,
|
|
246
|
+
useSolanaWallet
|
|
247
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { useState, useCallback } from "react";
|
|
3
|
+
|
|
4
|
+
// src/providers/SolanaProvider.ts
|
|
5
|
+
var useSolanaWallet = () => {
|
|
6
|
+
const getProvider = () => {
|
|
7
|
+
if ("solana" in window) {
|
|
8
|
+
const anyWindow = window;
|
|
9
|
+
if (anyWindow.solana?.isPhantom) {
|
|
10
|
+
return anyWindow.solana;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return void 0;
|
|
14
|
+
};
|
|
15
|
+
const connect = async () => {
|
|
16
|
+
const provider = getProvider();
|
|
17
|
+
if (provider) {
|
|
18
|
+
try {
|
|
19
|
+
const resp = await provider.connect();
|
|
20
|
+
return resp.publicKey.toString();
|
|
21
|
+
} catch (err) {
|
|
22
|
+
throw new Error("User rejected the connection");
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
throw new Error("Phantom wallet not found");
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
const sendPayment = async (intent) => {
|
|
29
|
+
const provider = getProvider();
|
|
30
|
+
if (!provider) throw new Error("Wallet not connected");
|
|
31
|
+
console.log(`Simulating Solana payment to ${intent.paymentAddress} for ${intent.amount} SOL`);
|
|
32
|
+
return "simulated_solana_signature_" + Math.random().toString(36).slice(2);
|
|
33
|
+
};
|
|
34
|
+
return { isAvailable: !!getProvider(), connect, sendPayment };
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// src/PaymentWidget.tsx
|
|
38
|
+
import { Wallet, CheckCircle, Loader2, AlertCircle, ExternalLink, Cpu } from "lucide-react";
|
|
39
|
+
import { clsx } from "clsx";
|
|
40
|
+
import { twMerge } from "tailwind-merge";
|
|
41
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
42
|
+
function cn(...inputs) {
|
|
43
|
+
return twMerge(clsx(inputs));
|
|
44
|
+
}
|
|
45
|
+
var PaymentWidget = ({
|
|
46
|
+
className,
|
|
47
|
+
...props
|
|
48
|
+
}) => {
|
|
49
|
+
const { status, intent, error, initiate, verify, setStatus, isSolanaAvailable, handleSolanaPay } = usePayment(props);
|
|
50
|
+
if (status === "success") {
|
|
51
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("p-6 text-center bg-green-50 rounded-xl border border-green-200", className), children: [
|
|
52
|
+
/* @__PURE__ */ jsx(CheckCircle, { className: "mx-auto w-12 h-12 text-green-500 mb-4" }),
|
|
53
|
+
/* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-green-900", children: "Payment Successful" }),
|
|
54
|
+
/* @__PURE__ */ jsx("p", { className: "text-green-700 mt-1", children: "Your order has been confirmed." })
|
|
55
|
+
] });
|
|
56
|
+
}
|
|
57
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden", className), children: [
|
|
58
|
+
/* @__PURE__ */ jsxs("div", { className: "p-6", children: [
|
|
59
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 mb-6", children: [
|
|
60
|
+
/* @__PURE__ */ jsx("div", { className: "p-2 bg-indigo-50 rounded-lg", children: /* @__PURE__ */ jsx(Wallet, { className: "w-5 h-5 text-indigo-600" }) }),
|
|
61
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
62
|
+
/* @__PURE__ */ jsx("h3", { className: "font-semibold text-slate-900", children: "Crypto Payment" }),
|
|
63
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Pay securely using your wallet" })
|
|
64
|
+
] })
|
|
65
|
+
] }),
|
|
66
|
+
error && /* @__PURE__ */ jsxs("div", { className: "mb-4 p-3 bg-red-50 border border-red-100 rounded-lg flex gap-3 text-red-700 text-sm", children: [
|
|
67
|
+
/* @__PURE__ */ jsx(AlertCircle, { className: "w-4 h-4 shrink-0" }),
|
|
68
|
+
/* @__PURE__ */ jsx("p", { children: error })
|
|
69
|
+
] }),
|
|
70
|
+
status === "idle" && /* @__PURE__ */ jsx(
|
|
71
|
+
"button",
|
|
72
|
+
{
|
|
73
|
+
onClick: initiate,
|
|
74
|
+
className: "w-full py-3 px-4 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-xl transition-colors flex items-center justify-center gap-2",
|
|
75
|
+
children: "Start Payment"
|
|
76
|
+
}
|
|
77
|
+
),
|
|
78
|
+
(status === "connecting" || status === "initiating" || status === "verifying") && /* @__PURE__ */ jsxs("div", { className: "py-8 text-center", children: [
|
|
79
|
+
/* @__PURE__ */ jsx(Loader2, { className: "w-8 h-8 text-indigo-600 animate-spin mx-auto mb-3" }),
|
|
80
|
+
/* @__PURE__ */ jsx("p", { className: "text-slate-600 font-medium", children: status === "connecting" ? "Connecting wallet..." : status === "initiating" ? "Preparing transaction..." : "Verifying on-chain..." })
|
|
81
|
+
] }),
|
|
82
|
+
status === "pending_signature" && intent && /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
83
|
+
/* @__PURE__ */ jsxs("div", { className: "p-4 bg-slate-50 rounded-xl space-y-2", children: [
|
|
84
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-between text-sm", children: [
|
|
85
|
+
/* @__PURE__ */ jsx("span", { className: "text-slate-500", children: "Amount to pay" }),
|
|
86
|
+
/* @__PURE__ */ jsxs("span", { className: "font-mono font-medium text-slate-900", children: [
|
|
87
|
+
intent.amount,
|
|
88
|
+
" ",
|
|
89
|
+
intent.currency
|
|
90
|
+
] })
|
|
91
|
+
] }),
|
|
92
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-between text-sm", children: [
|
|
93
|
+
/* @__PURE__ */ jsx("span", { className: "text-slate-500", children: "Network" }),
|
|
94
|
+
/* @__PURE__ */ jsx("span", { className: "text-slate-900 font-medium", children: intent.network })
|
|
95
|
+
] })
|
|
96
|
+
] }),
|
|
97
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
98
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-slate-500 uppercase font-bold tracking-wider", children: "Destination Address" }),
|
|
99
|
+
/* @__PURE__ */ jsx("div", { className: "p-3 bg-slate-100 rounded-lg font-mono text-xs break-all text-slate-600 border border-slate-200", children: intent.paymentAddress })
|
|
100
|
+
] }),
|
|
101
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2", children: intent.network.toLowerCase().includes("solana") && isSolanaAvailable ? /* @__PURE__ */ jsxs(
|
|
102
|
+
"button",
|
|
103
|
+
{
|
|
104
|
+
onClick: handleSolanaPay,
|
|
105
|
+
className: "w-full py-3 px-4 bg-[#512da8] hover:bg-[#4527a0] text-white font-medium rounded-xl transition-colors flex items-center justify-center gap-2",
|
|
106
|
+
children: [
|
|
107
|
+
/* @__PURE__ */ jsx(Cpu, { className: "w-4 h-4" }),
|
|
108
|
+
"Pay with Phantom"
|
|
109
|
+
]
|
|
110
|
+
}
|
|
111
|
+
) : /* @__PURE__ */ jsx(
|
|
112
|
+
"button",
|
|
113
|
+
{
|
|
114
|
+
onClick: () => {
|
|
115
|
+
setStatus("verifying");
|
|
116
|
+
setTimeout(() => verify("0x_mock_transaction_hash_generic"), 2e3);
|
|
117
|
+
},
|
|
118
|
+
className: "w-full py-3 px-4 bg-slate-900 hover:bg-black text-white font-medium rounded-xl transition-colors flex items-center justify-center gap-2",
|
|
119
|
+
children: "Confirm in Wallet"
|
|
120
|
+
}
|
|
121
|
+
) })
|
|
122
|
+
] })
|
|
123
|
+
] }),
|
|
124
|
+
/* @__PURE__ */ jsxs("div", { className: "bg-slate-50 px-6 py-3 border-t border-slate-100 flex justify-between items-center", children: [
|
|
125
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-slate-400", children: "Powered by Micro-CMS Crypto" }),
|
|
126
|
+
/* @__PURE__ */ jsxs("a", { href: "#", className: "text-xs text-indigo-600 hover:text-indigo-700 font-medium flex items-center gap-1", children: [
|
|
127
|
+
"Help ",
|
|
128
|
+
/* @__PURE__ */ jsx(ExternalLink, { className: "w-3 h-3" })
|
|
129
|
+
] })
|
|
130
|
+
] })
|
|
131
|
+
] });
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// src/index.ts
|
|
135
|
+
var usePayment = (props) => {
|
|
136
|
+
const [status, setStatus] = useState("idle");
|
|
137
|
+
const [error, setError] = useState(null);
|
|
138
|
+
const [intent, setIntent] = useState(null);
|
|
139
|
+
const initiate = useCallback(async () => {
|
|
140
|
+
try {
|
|
141
|
+
setStatus("initiating");
|
|
142
|
+
setError(null);
|
|
143
|
+
const response = await fetch(props.endpoints?.initiate || "/api/orders/initiate", {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: { "Content-Type": "application/json" },
|
|
146
|
+
body: JSON.stringify({
|
|
147
|
+
orderId: props.orderId,
|
|
148
|
+
amount: props.amount,
|
|
149
|
+
currency: props.currency
|
|
150
|
+
})
|
|
151
|
+
});
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
const errorData = await response.json();
|
|
154
|
+
throw new Error(errorData.error || "Failed to initiate payment intent");
|
|
155
|
+
}
|
|
156
|
+
const data = await response.json();
|
|
157
|
+
setIntent(data);
|
|
158
|
+
setStatus("pending_signature");
|
|
159
|
+
} catch (err) {
|
|
160
|
+
setError(err.message);
|
|
161
|
+
setStatus("error");
|
|
162
|
+
props.onError?.(err);
|
|
163
|
+
}
|
|
164
|
+
}, [props.endpoints?.initiate, props.orderId, props.amount, props.currency, props.onError]);
|
|
165
|
+
const verify = useCallback(async (txHash) => {
|
|
166
|
+
try {
|
|
167
|
+
setStatus("verifying");
|
|
168
|
+
const response = await fetch(props.endpoints?.verify || "/api/orders/verify-payment", {
|
|
169
|
+
method: "POST",
|
|
170
|
+
headers: { "Content-Type": "application/json" },
|
|
171
|
+
body: JSON.stringify({
|
|
172
|
+
orderId: props.orderId,
|
|
173
|
+
transactionHash: txHash
|
|
174
|
+
})
|
|
175
|
+
});
|
|
176
|
+
if (!response.ok) {
|
|
177
|
+
const errorData = await response.json();
|
|
178
|
+
throw new Error(errorData.error || "Payment verification failed");
|
|
179
|
+
}
|
|
180
|
+
const data = await response.json();
|
|
181
|
+
if (data.status === "confirmed") {
|
|
182
|
+
setStatus("success");
|
|
183
|
+
props.onSuccess?.(data);
|
|
184
|
+
} else {
|
|
185
|
+
throw new Error("Payment not confirmed yet, status: " + data.status);
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
setError(err.message);
|
|
189
|
+
setStatus("error");
|
|
190
|
+
props.onError?.(err);
|
|
191
|
+
}
|
|
192
|
+
}, [props.endpoints?.verify, props.orderId, props.onSuccess, props.onError]);
|
|
193
|
+
const { isAvailable: isSolanaAvailable, connect: connectSolana, sendPayment: sendSolanaPayment } = useSolanaWallet();
|
|
194
|
+
const handleSolanaPay = async () => {
|
|
195
|
+
try {
|
|
196
|
+
if (!intent) {
|
|
197
|
+
setError("Payment intent not established.");
|
|
198
|
+
setStatus("error");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
setStatus("connecting");
|
|
202
|
+
const publicKey = await connectSolana();
|
|
203
|
+
console.log("Connected to Solana with public key:", publicKey);
|
|
204
|
+
setStatus("pending_signature");
|
|
205
|
+
const signature = await sendSolanaPayment(intent);
|
|
206
|
+
setStatus("verifying");
|
|
207
|
+
await verify(signature);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
setError(err.message || "Failed to connect to Phantom or sign transaction.");
|
|
210
|
+
setStatus("error");
|
|
211
|
+
props.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
return { status, intent, error, initiate, verify, setStatus, isSolanaAvailable, handleSolanaPay };
|
|
215
|
+
};
|
|
216
|
+
export {
|
|
217
|
+
PaymentWidget,
|
|
218
|
+
usePayment,
|
|
219
|
+
useSolanaWallet
|
|
220
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@micro-cms/crypto-payments",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Composable Crypto Payment Widget for Micro-CMS",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
13
|
+
"dev": "tsup src/index.ts --format cjs,esm --watch --dts",
|
|
14
|
+
"lint": "eslint src/**/*.ts"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"lucide-react": "^0.344.0",
|
|
18
|
+
"clsx": "^2.1.0",
|
|
19
|
+
"tailwind-merge": "^2.2.1"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"react": "^18.0.0",
|
|
23
|
+
"@micro-cms/types": "workspace:*",
|
|
24
|
+
"@micro-cms/core": "workspace:*"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/react": "^18.2.0",
|
|
28
|
+
"typescript": "^5.3.3",
|
|
29
|
+
"tsup": "^8.0.2"
|
|
30
|
+
}
|
|
31
|
+
}
|