coinley-test 0.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/.github/workflows/azure-static-web-apps-gray-river-06ca63610.yml +48 -0
- package/README.md +8 -0
- package/eslint.config.js +38 -0
- package/index.html +13 -0
- package/package.json +42 -0
- package/postcss.config.js +6 -0
- package/public/Logomark.png +0 -0
- package/public/vite.svg +1 -0
- package/src/CoinleyPayment.jsx +787 -0
- package/src/CoinleyProvider.jsx +166 -0
- package/src/PaymentAPI.js +233 -0
- package/src/assets/Henry's Resume (1).pdf +0 -0
- package/src/assets/Invoice 0.2.pdf +0 -0
- package/src/assets/Logomark.png +0 -0
- package/src/assets/about.jpg +0 -0
- package/src/assets/airbnb.jpg +0 -0
- package/src/assets/calculateload.jpg +0 -0
- package/src/assets/coinbase.jpg +0 -0
- package/src/assets/coinley-store-dark.svg +22 -0
- package/src/assets/coinley-store.svg +22 -0
- package/src/assets/hellosign.jpg +0 -0
- package/src/assets/hero.jpg +0 -0
- package/src/assets/medium.jpg +0 -0
- package/src/assets/panel.jpg +0 -0
- package/src/assets/react.svg +1 -0
- package/src/assets/selectappliances.jpg +0 -0
- package/src/assets/upwork.jpg +0 -0
- package/src/assets/zapier.jpg +0 -0
- package/src/index.css +3 -0
- package/src/index.js +70 -0
- package/src/main.jsx +20 -0
- package/src/styles/animations.css +180 -0
- package/src/styles/index.css +94 -0
- package/src/url.js +1 -0
- package/staticwebapp.config.json +8 -0
- package/tailwind.config.js +31 -0
- package/vercel.json +8 -0
- package/vite.config.js +34 -0
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
//src/CoinleyPayment.jsx
|
|
2
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
3
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
4
|
+
import {
|
|
5
|
+
Wallet,
|
|
6
|
+
QrCode,
|
|
7
|
+
ChevronDown,
|
|
8
|
+
Copy,
|
|
9
|
+
Check,
|
|
10
|
+
X,
|
|
11
|
+
ArrowLeft,
|
|
12
|
+
Loader2,
|
|
13
|
+
ExternalLink,
|
|
14
|
+
AlertCircle,
|
|
15
|
+
CheckCircle2,
|
|
16
|
+
Zap,
|
|
17
|
+
Sparkles
|
|
18
|
+
} from 'lucide-react';
|
|
19
|
+
import QRCodeLib from 'qrcode';
|
|
20
|
+
import {
|
|
21
|
+
createCoinleyWalletConfig,
|
|
22
|
+
CoinleyWalletProvider,
|
|
23
|
+
ConnectButton,
|
|
24
|
+
WalletModal,
|
|
25
|
+
useWallet,
|
|
26
|
+
useWalletConnect,
|
|
27
|
+
useWalletTransaction,
|
|
28
|
+
useWalletModal
|
|
29
|
+
} from '@coinley/wallet-connect-core';
|
|
30
|
+
import { PaymentAPI } from './PaymentAPI';
|
|
31
|
+
|
|
32
|
+
// Wallet Integration Component
|
|
33
|
+
const WalletIntegration = ({
|
|
34
|
+
selectedNetwork,
|
|
35
|
+
selectedToken,
|
|
36
|
+
paymentData,
|
|
37
|
+
onTransactionSent,
|
|
38
|
+
onError,
|
|
39
|
+
isConnecting,
|
|
40
|
+
setIsConnecting
|
|
41
|
+
}) => {
|
|
42
|
+
const { isConnected, address } = useWallet();
|
|
43
|
+
const { connectWallet } = useWalletConnect();
|
|
44
|
+
const { sendTransaction } = useWalletTransaction();
|
|
45
|
+
const { openModal } = useWalletModal();
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (isConnected && paymentData && selectedNetwork && selectedToken) {
|
|
49
|
+
handleSendTransaction();
|
|
50
|
+
}
|
|
51
|
+
}, [isConnected, paymentData]);
|
|
52
|
+
|
|
53
|
+
const handleConnectWallet = async () => {
|
|
54
|
+
try {
|
|
55
|
+
setIsConnecting(true);
|
|
56
|
+
openModal();
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('Wallet connection failed:', error);
|
|
59
|
+
onError(error.message || 'Failed to connect wallet');
|
|
60
|
+
setIsConnecting(false);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const handleSendTransaction = async () => {
|
|
65
|
+
if (!isConnected || !paymentData || !selectedToken) return;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
setIsConnecting(true);
|
|
69
|
+
|
|
70
|
+
const recipientAddress = paymentData.metadata?.recipientWallet;
|
|
71
|
+
if (!recipientAddress) {
|
|
72
|
+
throw new Error('Recipient wallet address not found');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let txHash;
|
|
76
|
+
|
|
77
|
+
if (selectedToken.contractAddress) {
|
|
78
|
+
// ERC-20 Token Transaction
|
|
79
|
+
const amount = (paymentData.totalAmount * Math.pow(10, selectedToken.decimals)).toString();
|
|
80
|
+
const transferData = `0xa9059cbb${recipientAddress.slice(2).padStart(64, '0')}${parseInt(amount).toString(16).padStart(64, '0')}`;
|
|
81
|
+
|
|
82
|
+
txHash = await sendTransaction({
|
|
83
|
+
to: selectedToken.contractAddress,
|
|
84
|
+
data: transferData,
|
|
85
|
+
value: '0x0'
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
// Native Token Transaction
|
|
89
|
+
const value = (paymentData.totalAmount * Math.pow(10, 18)).toString(16);
|
|
90
|
+
|
|
91
|
+
txHash = await sendTransaction({
|
|
92
|
+
to: recipientAddress,
|
|
93
|
+
value: `0x${value}`
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (txHash) {
|
|
98
|
+
onTransactionSent(txHash);
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.error('Transaction failed:', error);
|
|
102
|
+
onError(error.message || 'Transaction failed');
|
|
103
|
+
} finally {
|
|
104
|
+
setIsConnecting(false);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className="text-center space-y-6">
|
|
110
|
+
<div className="space-y-4">
|
|
111
|
+
<div className="w-16 h-16 bg-[#7042D2] bg-opacity-20 rounded-full flex items-center justify-center mx-auto">
|
|
112
|
+
<Wallet className="w-8 h-8 text-[#7042D2]" />
|
|
113
|
+
</div>
|
|
114
|
+
<h3 className="text-lg font-semibold">
|
|
115
|
+
{isConnected ? 'Send Payment' : 'Connect Your Wallet'}
|
|
116
|
+
</h3>
|
|
117
|
+
|
|
118
|
+
{!isConnected ? (
|
|
119
|
+
<div className="space-y-4">
|
|
120
|
+
<p className="text-gray-600 dark:text-gray-400">
|
|
121
|
+
Connect your wallet to complete the payment
|
|
122
|
+
</p>
|
|
123
|
+
<ConnectButton
|
|
124
|
+
theme="gradient"
|
|
125
|
+
size="large"
|
|
126
|
+
customText="Connect Wallet"
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
) : (
|
|
130
|
+
<div className="space-y-4">
|
|
131
|
+
<p className="text-green-600 dark:text-green-400">
|
|
132
|
+
✅ Wallet Connected: {address?.slice(0, 6)}...{address?.slice(-4)}
|
|
133
|
+
</p>
|
|
134
|
+
|
|
135
|
+
{paymentData && (
|
|
136
|
+
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 space-y-2">
|
|
137
|
+
<div className="flex justify-between items-center">
|
|
138
|
+
<span className="text-sm text-gray-600 dark:text-gray-400">Amount:</span>
|
|
139
|
+
<span className="font-medium">{paymentData.totalAmount} {selectedToken?.symbol}</span>
|
|
140
|
+
</div>
|
|
141
|
+
<div className="flex justify-between items-center">
|
|
142
|
+
<span className="text-sm text-gray-600 dark:text-gray-400">Network:</span>
|
|
143
|
+
<span className="font-medium">{selectedNetwork?.name}</span>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
{isConnecting && (
|
|
149
|
+
<div className="flex items-center justify-center space-x-2">
|
|
150
|
+
<Loader2 className="w-5 h-5 animate-spin text-[#7042D2]" />
|
|
151
|
+
<span>Processing transaction...</span>
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const CoinleyPayment = ({
|
|
162
|
+
apiKey,
|
|
163
|
+
apiSecret,
|
|
164
|
+
apiUrl = 'http://localhost:9000',
|
|
165
|
+
onSuccess,
|
|
166
|
+
onError,
|
|
167
|
+
onClose,
|
|
168
|
+
isOpen,
|
|
169
|
+
config,
|
|
170
|
+
theme = 'light',
|
|
171
|
+
merchantName,
|
|
172
|
+
debug = false
|
|
173
|
+
}) => {
|
|
174
|
+
// State management
|
|
175
|
+
const [currentStep, setCurrentStep] = useState('select-method');
|
|
176
|
+
const [networks, setNetworks] = useState([]);
|
|
177
|
+
const [tokens, setTokens] = useState([]);
|
|
178
|
+
const [selectedNetwork, setSelectedNetwork] = useState(null);
|
|
179
|
+
const [selectedToken, setSelectedToken] = useState(null);
|
|
180
|
+
const [paymentMethod, setPaymentMethod] = useState(null);
|
|
181
|
+
const [qrCode, setQrCode] = useState('');
|
|
182
|
+
const [paymentData, setPaymentData] = useState(null);
|
|
183
|
+
const [loading, setLoading] = useState(false);
|
|
184
|
+
const [error, setError] = useState('');
|
|
185
|
+
const [copied, setCopied] = useState(false);
|
|
186
|
+
const [txHash, setTxHash] = useState('');
|
|
187
|
+
const [isConnecting, setIsConnecting] = useState(false);
|
|
188
|
+
|
|
189
|
+
// Services
|
|
190
|
+
const paymentAPI = useRef(new PaymentAPI(apiUrl, apiKey, apiSecret));
|
|
191
|
+
|
|
192
|
+
// Wallet configuration
|
|
193
|
+
const walletConfig = createCoinleyWalletConfig({
|
|
194
|
+
appName: merchantName || 'Coinley Payment',
|
|
195
|
+
appDescription: 'Crypto payment processing',
|
|
196
|
+
chains: ['ethereum', 'polygon', 'bsc', 'arbitrum']
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Initialize
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
if (isOpen) {
|
|
202
|
+
initializePayment();
|
|
203
|
+
}
|
|
204
|
+
}, [isOpen]);
|
|
205
|
+
|
|
206
|
+
const initializePayment = async () => {
|
|
207
|
+
try {
|
|
208
|
+
setLoading(true);
|
|
209
|
+
setError('');
|
|
210
|
+
|
|
211
|
+
const [networksRes, tokensRes] = await Promise.all([
|
|
212
|
+
paymentAPI.current.getNetworks(),
|
|
213
|
+
paymentAPI.current.getStablecoins()
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
setNetworks(networksRes.networks || []);
|
|
217
|
+
setTokens(tokensRes.stablecoins || []);
|
|
218
|
+
|
|
219
|
+
if (debug) {
|
|
220
|
+
console.log('Initialized networks:', networksRes.networks);
|
|
221
|
+
console.log('Initialized tokens:', tokensRes.stablecoins);
|
|
222
|
+
}
|
|
223
|
+
} catch (err) {
|
|
224
|
+
console.error('Failed to initialize payment:', err);
|
|
225
|
+
setError('Failed to load payment options. Please try again.');
|
|
226
|
+
} finally {
|
|
227
|
+
setLoading(false);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const handleMethodSelect = (method) => {
|
|
232
|
+
setPaymentMethod(method);
|
|
233
|
+
setCurrentStep('select-network');
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const handleNetworkSelect = async (network) => {
|
|
237
|
+
setSelectedNetwork(network);
|
|
238
|
+
|
|
239
|
+
const networkTokens = tokens.filter(token =>
|
|
240
|
+
token.Network?.shortName === network.shortName || token.networkId === network.id
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if (networkTokens.length > 0) {
|
|
244
|
+
const preferredToken = networkTokens.find(t => t.symbol === 'USDT') || networkTokens[0];
|
|
245
|
+
setSelectedToken(preferredToken);
|
|
246
|
+
await initiatePayment(network, preferredToken);
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const initiatePayment = async (network, token) => {
|
|
251
|
+
try {
|
|
252
|
+
setLoading(true);
|
|
253
|
+
|
|
254
|
+
const paymentPayload = {
|
|
255
|
+
amount: config.amount,
|
|
256
|
+
currency: token.symbol,
|
|
257
|
+
network: network.shortName,
|
|
258
|
+
customerEmail: config.customerEmail,
|
|
259
|
+
callbackUrl: config.callbackUrl,
|
|
260
|
+
metadata: {
|
|
261
|
+
...config.metadata,
|
|
262
|
+
paymentMethod,
|
|
263
|
+
selectedNetwork: network.shortName,
|
|
264
|
+
selectedToken: token.symbol
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const payment = await paymentAPI.current.createPayment(paymentPayload);
|
|
269
|
+
setPaymentData(payment.payment);
|
|
270
|
+
|
|
271
|
+
if (paymentMethod === 'wallet') {
|
|
272
|
+
setCurrentStep('wallet-connect');
|
|
273
|
+
} else {
|
|
274
|
+
await generateQRCode(payment.payment);
|
|
275
|
+
setCurrentStep('qr-code');
|
|
276
|
+
}
|
|
277
|
+
} catch (err) {
|
|
278
|
+
console.error('Payment initiation failed:', err);
|
|
279
|
+
setError(err.message || 'Failed to create payment');
|
|
280
|
+
setCurrentStep('error');
|
|
281
|
+
} finally {
|
|
282
|
+
setLoading(false);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const generateQRCode = async (payment) => {
|
|
287
|
+
try {
|
|
288
|
+
const recipientAddress = payment.metadata?.recipientWallet ||
|
|
289
|
+
config.merchantWalletAddresses?.[selectedNetwork?.shortName || ''];
|
|
290
|
+
|
|
291
|
+
if (!recipientAddress) {
|
|
292
|
+
throw new Error('Recipient wallet address not found');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let qrData = '';
|
|
296
|
+
|
|
297
|
+
if (selectedNetwork?.type === 'ethereum' || selectedNetwork?.type === 'bsc') {
|
|
298
|
+
qrData = selectedToken?.contractAddress
|
|
299
|
+
? `ethereum:${selectedToken.contractAddress}/transfer?address=${recipientAddress}&uint256=${payment.totalAmount * Math.pow(10, selectedToken.decimals)}`
|
|
300
|
+
: `ethereum:${recipientAddress}?value=${payment.totalAmount}e18`;
|
|
301
|
+
} else if (selectedNetwork?.type === 'tron') {
|
|
302
|
+
qrData = `tron:${recipientAddress}?amount=${payment.totalAmount}`;
|
|
303
|
+
} else {
|
|
304
|
+
qrData = `${selectedToken?.symbol}:${recipientAddress}?amount=${payment.totalAmount}`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const qrCodeDataURL = await QRCodeLib.toDataURL(qrData, {
|
|
308
|
+
width: 256,
|
|
309
|
+
margin: 2,
|
|
310
|
+
color: {
|
|
311
|
+
dark: theme === 'dark' ? '#FFFFFF' : '#000000',
|
|
312
|
+
light: theme === 'dark' ? '#000000' : '#FFFFFF'
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
setQrCode(qrCodeDataURL);
|
|
317
|
+
startPaymentVerification(payment.id);
|
|
318
|
+
|
|
319
|
+
} catch (err) {
|
|
320
|
+
console.error('QR code generation failed:', err);
|
|
321
|
+
setError('Failed to generate QR code');
|
|
322
|
+
setCurrentStep('error');
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const startPaymentVerification = async (paymentId) => {
|
|
327
|
+
const maxAttempts = 60;
|
|
328
|
+
let attempts = 0;
|
|
329
|
+
|
|
330
|
+
const checkPayment = async () => {
|
|
331
|
+
try {
|
|
332
|
+
const result = await paymentAPI.current.verifyQRPayment(paymentId);
|
|
333
|
+
|
|
334
|
+
if (result.verified && result.payment?.transactionHash) {
|
|
335
|
+
setTxHash(result.payment.transactionHash);
|
|
336
|
+
await handlePaymentSuccess(paymentId, result.payment.transactionHash);
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
attempts++;
|
|
341
|
+
if (attempts < maxAttempts) {
|
|
342
|
+
setTimeout(checkPayment, 5000);
|
|
343
|
+
} else {
|
|
344
|
+
setError('Payment verification timeout. Please check your transaction.');
|
|
345
|
+
setCurrentStep('error');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return false;
|
|
349
|
+
} catch (err) {
|
|
350
|
+
console.error('Payment verification error:', err);
|
|
351
|
+
attempts++;
|
|
352
|
+
if (attempts < maxAttempts) {
|
|
353
|
+
setTimeout(checkPayment, 5000);
|
|
354
|
+
} else {
|
|
355
|
+
setError('Payment verification failed');
|
|
356
|
+
setCurrentStep('error');
|
|
357
|
+
}
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
setTimeout(checkPayment, 2000);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const handleTransactionSent = async (transactionHash) => {
|
|
366
|
+
setTxHash(transactionHash);
|
|
367
|
+
setCurrentStep('processing');
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
await paymentAPI.current.processPayment(
|
|
371
|
+
paymentData.id,
|
|
372
|
+
transactionHash,
|
|
373
|
+
selectedNetwork?.shortName || '',
|
|
374
|
+
''
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
await handlePaymentSuccess(paymentData.id, transactionHash);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
console.error('Payment processing failed:', error);
|
|
380
|
+
await handlePaymentSuccess(paymentData.id, transactionHash);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const handlePaymentSuccess = async (paymentId, transactionHash) => {
|
|
385
|
+
setCurrentStep('success');
|
|
386
|
+
|
|
387
|
+
if (onSuccess) {
|
|
388
|
+
onSuccess(paymentId, transactionHash, {
|
|
389
|
+
network: selectedNetwork?.name,
|
|
390
|
+
token: selectedToken?.symbol,
|
|
391
|
+
amount: paymentData?.totalAmount,
|
|
392
|
+
method: paymentMethod
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const copyToClipboard = async (text) => {
|
|
398
|
+
try {
|
|
399
|
+
await navigator.clipboard.writeText(text);
|
|
400
|
+
setCopied(true);
|
|
401
|
+
setTimeout(() => setCopied(false), 2000);
|
|
402
|
+
} catch (err) {
|
|
403
|
+
console.error('Failed to copy:', err);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const handleClose = () => {
|
|
408
|
+
setCurrentStep('select-method');
|
|
409
|
+
setPaymentMethod(null);
|
|
410
|
+
setSelectedNetwork(null);
|
|
411
|
+
setSelectedToken(null);
|
|
412
|
+
setQrCode('');
|
|
413
|
+
setPaymentData(null);
|
|
414
|
+
setError('');
|
|
415
|
+
setTxHash('');
|
|
416
|
+
|
|
417
|
+
if (onClose) {
|
|
418
|
+
onClose();
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const goBack = () => {
|
|
423
|
+
if (currentStep === 'select-network') {
|
|
424
|
+
setCurrentStep('select-method');
|
|
425
|
+
} else if (currentStep === 'wallet-connect' || currentStep === 'qr-code') {
|
|
426
|
+
setCurrentStep('select-network');
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
if (!isOpen) return null;
|
|
431
|
+
|
|
432
|
+
return (
|
|
433
|
+
<CoinleyWalletProvider config={walletConfig} walletConfig={{ appName: merchantName || 'Coinley Payment' }}>
|
|
434
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 backdrop-blur-sm">
|
|
435
|
+
<motion.div
|
|
436
|
+
initial={{ scale: 0.9, opacity: 0 }}
|
|
437
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
438
|
+
exit={{ scale: 0.9, opacity: 0 }}
|
|
439
|
+
className={`relative w-full max-w-md mx-4 rounded-2xl shadow-2xl overflow-hidden ${
|
|
440
|
+
theme === 'dark' ? 'bg-gray-900 text-white' : 'bg-white text-gray-900'
|
|
441
|
+
}`}
|
|
442
|
+
>
|
|
443
|
+
{/* Header */}
|
|
444
|
+
<div className="relative p-6 border-b border-gray-200 dark:border-gray-700">
|
|
445
|
+
{(currentStep !== 'select-method' && currentStep !== 'success' && currentStep !== 'error') && (
|
|
446
|
+
<button
|
|
447
|
+
onClick={goBack}
|
|
448
|
+
className="absolute left-4 top-6 p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
449
|
+
>
|
|
450
|
+
<ArrowLeft className="w-5 h-5" />
|
|
451
|
+
</button>
|
|
452
|
+
)}
|
|
453
|
+
|
|
454
|
+
<button
|
|
455
|
+
onClick={handleClose}
|
|
456
|
+
className="absolute right-4 top-6 p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
457
|
+
>
|
|
458
|
+
<X className="w-5 h-5" />
|
|
459
|
+
</button>
|
|
460
|
+
|
|
461
|
+
<div className="text-center">
|
|
462
|
+
<div className="flex items-center justify-center space-x-2 mb-2">
|
|
463
|
+
<Sparkles className="w-5 h-5 text-[#7042D2]" />
|
|
464
|
+
<h2 className="text-xl font-bold">Pay with Crypto</h2>
|
|
465
|
+
<Sparkles className="w-5 h-5 text-[#7042D2]" />
|
|
466
|
+
</div>
|
|
467
|
+
{merchantName && (
|
|
468
|
+
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
|
469
|
+
{merchantName}
|
|
470
|
+
</p>
|
|
471
|
+
)}
|
|
472
|
+
<div className="mt-2">
|
|
473
|
+
<span className="text-2xl font-bold text-[#7042D2]">
|
|
474
|
+
${config.amount.toFixed(2)}
|
|
475
|
+
</span>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
{/* Content */}
|
|
481
|
+
<div className="p-6 min-h-[400px]">
|
|
482
|
+
<AnimatePresence mode="wait">
|
|
483
|
+
{/* Step 1: Select Payment Method */}
|
|
484
|
+
{currentStep === 'select-method' && (
|
|
485
|
+
<motion.div
|
|
486
|
+
key="select-method"
|
|
487
|
+
initial={{ x: 20, opacity: 0 }}
|
|
488
|
+
animate={{ x: 0, opacity: 1 }}
|
|
489
|
+
exit={{ x: -20, opacity: 0 }}
|
|
490
|
+
className="space-y-4"
|
|
491
|
+
>
|
|
492
|
+
<h3 className="text-lg font-semibold text-center mb-6">
|
|
493
|
+
Choose Payment Method
|
|
494
|
+
</h3>
|
|
495
|
+
|
|
496
|
+
<motion.button
|
|
497
|
+
whileHover={{ scale: 1.02 }}
|
|
498
|
+
whileTap={{ scale: 0.98 }}
|
|
499
|
+
onClick={() => handleMethodSelect('wallet')}
|
|
500
|
+
className="w-full p-4 border-2 border-gray-200 dark:border-gray-700 rounded-xl hover:border-[#7042D2] transition-colors group"
|
|
501
|
+
>
|
|
502
|
+
<div className="flex items-center space-x-4">
|
|
503
|
+
<div className="p-3 bg-[#7042D2] bg-opacity-10 rounded-lg group-hover:bg-opacity-20 transition-colors">
|
|
504
|
+
<Wallet className="w-6 h-6 text-[#7042D2]" />
|
|
505
|
+
</div>
|
|
506
|
+
<div className="text-left">
|
|
507
|
+
<div className="font-semibold">Connect Wallet</div>
|
|
508
|
+
<div className="text-sm text-gray-600 dark:text-gray-400">
|
|
509
|
+
MetaMask, WalletConnect & more
|
|
510
|
+
</div>
|
|
511
|
+
</div>
|
|
512
|
+
<Zap className="w-5 h-5 text-[#7042D2] ml-auto" />
|
|
513
|
+
</div>
|
|
514
|
+
</motion.button>
|
|
515
|
+
|
|
516
|
+
<motion.button
|
|
517
|
+
whileHover={{ scale: 1.02 }}
|
|
518
|
+
whileTap={{ scale: 0.98 }}
|
|
519
|
+
onClick={() => handleMethodSelect('qr')}
|
|
520
|
+
className="w-full p-4 border-2 border-gray-200 dark:border-gray-700 rounded-xl hover:border-[#7042D2] transition-colors group"
|
|
521
|
+
>
|
|
522
|
+
<div className="flex items-center space-x-4">
|
|
523
|
+
<div className="p-3 bg-[#7042D2] bg-opacity-10 rounded-lg group-hover:bg-opacity-20 transition-colors">
|
|
524
|
+
<QrCode className="w-6 h-6 text-[#7042D2]" />
|
|
525
|
+
</div>
|
|
526
|
+
<div className="text-left">
|
|
527
|
+
<div className="font-semibold">QR Code</div>
|
|
528
|
+
<div className="text-sm text-gray-600 dark:text-gray-400">
|
|
529
|
+
Scan with mobile wallet
|
|
530
|
+
</div>
|
|
531
|
+
</div>
|
|
532
|
+
</div>
|
|
533
|
+
</motion.button>
|
|
534
|
+
</motion.div>
|
|
535
|
+
)}
|
|
536
|
+
|
|
537
|
+
{/* Step 2: Select Network */}
|
|
538
|
+
{currentStep === 'select-network' && (
|
|
539
|
+
<motion.div
|
|
540
|
+
key="select-network"
|
|
541
|
+
initial={{ x: 20, opacity: 0 }}
|
|
542
|
+
animate={{ x: 0, opacity: 1 }}
|
|
543
|
+
exit={{ x: -20, opacity: 0 }}
|
|
544
|
+
className="space-y-4"
|
|
545
|
+
>
|
|
546
|
+
<h3 className="text-lg font-semibold text-center mb-6">
|
|
547
|
+
Select Network
|
|
548
|
+
</h3>
|
|
549
|
+
|
|
550
|
+
{loading ? (
|
|
551
|
+
<div className="flex justify-center py-8">
|
|
552
|
+
<Loader2 className="w-8 h-8 animate-spin text-[#7042D2]" />
|
|
553
|
+
</div>
|
|
554
|
+
) : (
|
|
555
|
+
<div className="space-y-3">
|
|
556
|
+
{networks.map((network) => (
|
|
557
|
+
<motion.button
|
|
558
|
+
key={network.id}
|
|
559
|
+
whileHover={{ scale: 1.02 }}
|
|
560
|
+
whileTap={{ scale: 0.98 }}
|
|
561
|
+
onClick={() => handleNetworkSelect(network)}
|
|
562
|
+
className="w-full p-4 border-2 border-gray-200 dark:border-gray-700 rounded-xl hover:border-[#7042D2] transition-colors group"
|
|
563
|
+
>
|
|
564
|
+
<div className="flex items-center space-x-4">
|
|
565
|
+
{network.logo ? (
|
|
566
|
+
<img
|
|
567
|
+
src={network.logo}
|
|
568
|
+
alt={network.name}
|
|
569
|
+
className="w-8 h-8 rounded-full"
|
|
570
|
+
/>
|
|
571
|
+
) : (
|
|
572
|
+
<div className="w-8 h-8 bg-[#7042D2] bg-opacity-20 rounded-full flex items-center justify-center">
|
|
573
|
+
<span className="text-sm font-bold text-[#7042D2]">
|
|
574
|
+
{network.shortName.charAt(0).toUpperCase()}
|
|
575
|
+
</span>
|
|
576
|
+
</div>
|
|
577
|
+
)}
|
|
578
|
+
<div className="text-left flex-1">
|
|
579
|
+
<div className="font-semibold">{network.name}</div>
|
|
580
|
+
<div className="text-sm text-gray-600 dark:text-gray-400">
|
|
581
|
+
{network.shortName.toUpperCase()}
|
|
582
|
+
</div>
|
|
583
|
+
</div>
|
|
584
|
+
<ChevronDown className="w-5 h-5 text-gray-400 transform -rotate-90" />
|
|
585
|
+
</div>
|
|
586
|
+
</motion.button>
|
|
587
|
+
))}
|
|
588
|
+
</div>
|
|
589
|
+
)}
|
|
590
|
+
</motion.div>
|
|
591
|
+
)}
|
|
592
|
+
|
|
593
|
+
{/* Wallet Connect */}
|
|
594
|
+
{currentStep === 'wallet-connect' && (
|
|
595
|
+
<motion.div
|
|
596
|
+
key="wallet-connect"
|
|
597
|
+
initial={{ x: 20, opacity: 0 }}
|
|
598
|
+
animate={{ x: 0, opacity: 1 }}
|
|
599
|
+
exit={{ x: -20, opacity: 0 }}
|
|
600
|
+
>
|
|
601
|
+
<WalletIntegration
|
|
602
|
+
selectedNetwork={selectedNetwork}
|
|
603
|
+
selectedToken={selectedToken}
|
|
604
|
+
paymentData={paymentData}
|
|
605
|
+
onTransactionSent={handleTransactionSent}
|
|
606
|
+
onError={setError}
|
|
607
|
+
isConnecting={isConnecting}
|
|
608
|
+
setIsConnecting={setIsConnecting}
|
|
609
|
+
/>
|
|
610
|
+
</motion.div>
|
|
611
|
+
)}
|
|
612
|
+
|
|
613
|
+
{/* QR Code */}
|
|
614
|
+
{currentStep === 'qr-code' && (
|
|
615
|
+
<motion.div
|
|
616
|
+
key="qr-code"
|
|
617
|
+
initial={{ x: 20, opacity: 0 }}
|
|
618
|
+
animate={{ x: 0, opacity: 1 }}
|
|
619
|
+
exit={{ x: -20, opacity: 0 }}
|
|
620
|
+
className="text-center space-y-6"
|
|
621
|
+
>
|
|
622
|
+
<div className="space-y-4">
|
|
623
|
+
<h3 className="text-lg font-semibold">Scan QR Code</h3>
|
|
624
|
+
<p className="text-gray-600 dark:text-gray-400">
|
|
625
|
+
Use your mobile wallet to scan and pay
|
|
626
|
+
</p>
|
|
627
|
+
</div>
|
|
628
|
+
|
|
629
|
+
{qrCode && (
|
|
630
|
+
<div className="bg-white p-4 rounded-xl mx-auto inline-block">
|
|
631
|
+
<img src={qrCode} alt="Payment QR Code" className="w-48 h-48" />
|
|
632
|
+
</div>
|
|
633
|
+
)}
|
|
634
|
+
|
|
635
|
+
{paymentData && (
|
|
636
|
+
<div className="space-y-3">
|
|
637
|
+
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 space-y-2">
|
|
638
|
+
<div className="flex justify-between items-center">
|
|
639
|
+
<span className="text-sm text-gray-600 dark:text-gray-400">Network:</span>
|
|
640
|
+
<span className="font-medium">{selectedNetwork?.name}</span>
|
|
641
|
+
</div>
|
|
642
|
+
<div className="flex justify-between items-center">
|
|
643
|
+
<span className="text-sm text-gray-600 dark:text-gray-400">Amount:</span>
|
|
644
|
+
<span className="font-medium">{paymentData.totalAmount} {selectedToken?.symbol}</span>
|
|
645
|
+
</div>
|
|
646
|
+
</div>
|
|
647
|
+
|
|
648
|
+
<div className="flex items-center justify-center space-x-2 text-sm text-gray-600 dark:text-gray-400">
|
|
649
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
650
|
+
<span>Waiting for payment...</span>
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
653
|
+
)}
|
|
654
|
+
</motion.div>
|
|
655
|
+
)}
|
|
656
|
+
|
|
657
|
+
{/* Processing */}
|
|
658
|
+
{currentStep === 'processing' && (
|
|
659
|
+
<motion.div
|
|
660
|
+
key="processing"
|
|
661
|
+
initial={{ scale: 0.8, opacity: 0 }}
|
|
662
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
663
|
+
className="text-center space-y-6"
|
|
664
|
+
>
|
|
665
|
+
<div className="w-16 h-16 bg-[#7042D2] bg-opacity-20 rounded-full flex items-center justify-center mx-auto">
|
|
666
|
+
<Loader2 className="w-8 h-8 animate-spin text-[#7042D2]" />
|
|
667
|
+
</div>
|
|
668
|
+
<div>
|
|
669
|
+
<h3 className="text-lg font-semibold text-[#7042D2]">Processing Payment...</h3>
|
|
670
|
+
<p className="text-gray-600 dark:text-gray-400 mt-2">
|
|
671
|
+
Your transaction is being processed
|
|
672
|
+
</p>
|
|
673
|
+
</div>
|
|
674
|
+
</motion.div>
|
|
675
|
+
)}
|
|
676
|
+
|
|
677
|
+
{/* Success */}
|
|
678
|
+
{currentStep === 'success' && (
|
|
679
|
+
<motion.div
|
|
680
|
+
key="success"
|
|
681
|
+
initial={{ scale: 0.8, opacity: 0 }}
|
|
682
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
683
|
+
className="text-center space-y-6"
|
|
684
|
+
>
|
|
685
|
+
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto">
|
|
686
|
+
<CheckCircle2 className="w-8 h-8 text-green-500" />
|
|
687
|
+
</div>
|
|
688
|
+
<div>
|
|
689
|
+
<h3 className="text-lg font-semibold text-green-600">Payment Successful!</h3>
|
|
690
|
+
<p className="text-gray-600 dark:text-gray-400 mt-2">
|
|
691
|
+
Your payment has been processed successfully
|
|
692
|
+
</p>
|
|
693
|
+
</div>
|
|
694
|
+
|
|
695
|
+
{txHash && (
|
|
696
|
+
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
|
|
697
|
+
<div className="flex justify-between items-center">
|
|
698
|
+
<span className="text-sm text-gray-600 dark:text-gray-400">Transaction:</span>
|
|
699
|
+
<div className="flex items-center space-x-2">
|
|
700
|
+
<span className="font-mono text-sm">
|
|
701
|
+
{txHash.slice(0, 6)}...{txHash.slice(-4)}
|
|
702
|
+
</span>
|
|
703
|
+
<button
|
|
704
|
+
onClick={() => copyToClipboard(txHash)}
|
|
705
|
+
className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded"
|
|
706
|
+
>
|
|
707
|
+
<Copy className="w-4 h-4" />
|
|
708
|
+
</button>
|
|
709
|
+
{selectedNetwork?.explorerUrl && (
|
|
710
|
+
<a
|
|
711
|
+
href={`${selectedNetwork.explorerUrl}/tx/${txHash}`}
|
|
712
|
+
target="_blank"
|
|
713
|
+
rel="noopener noreferrer"
|
|
714
|
+
className="p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded"
|
|
715
|
+
>
|
|
716
|
+
<ExternalLink className="w-4 h-4" />
|
|
717
|
+
</a>
|
|
718
|
+
)}
|
|
719
|
+
</div>
|
|
720
|
+
</div>
|
|
721
|
+
</div>
|
|
722
|
+
)}
|
|
723
|
+
|
|
724
|
+
<button
|
|
725
|
+
onClick={handleClose}
|
|
726
|
+
className="w-full bg-[#7042D2] text-white py-3 rounded-lg font-semibold hover:bg-[#7042D2]/90 transition-colors"
|
|
727
|
+
>
|
|
728
|
+
Close
|
|
729
|
+
</button>
|
|
730
|
+
</motion.div>
|
|
731
|
+
)}
|
|
732
|
+
|
|
733
|
+
{/* Error */}
|
|
734
|
+
{currentStep === 'error' && (
|
|
735
|
+
<motion.div
|
|
736
|
+
key="error"
|
|
737
|
+
initial={{ scale: 0.8, opacity: 0 }}
|
|
738
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
739
|
+
className="text-center space-y-6"
|
|
740
|
+
>
|
|
741
|
+
<div className="w-16 h-16 bg-red-100 rounded-full flex items-center justify-center mx-auto">
|
|
742
|
+
<AlertCircle className="w-8 h-8 text-red-500" />
|
|
743
|
+
</div>
|
|
744
|
+
<div>
|
|
745
|
+
<h3 className="text-lg font-semibold text-red-600">Payment Failed</h3>
|
|
746
|
+
<p className="text-gray-600 dark:text-gray-400 mt-2">
|
|
747
|
+
{error || 'Something went wrong. Please try again.'}
|
|
748
|
+
</p>
|
|
749
|
+
</div>
|
|
750
|
+
|
|
751
|
+
<div className="space-y-3">
|
|
752
|
+
<button
|
|
753
|
+
onClick={() => {
|
|
754
|
+
setCurrentStep('select-method');
|
|
755
|
+
setError('');
|
|
756
|
+
}}
|
|
757
|
+
className="w-full bg-[#7042D2] text-white py-3 rounded-lg font-semibold hover:bg-[#7042D2]/90 transition-colors"
|
|
758
|
+
>
|
|
759
|
+
Try Again
|
|
760
|
+
</button>
|
|
761
|
+
<button
|
|
762
|
+
onClick={handleClose}
|
|
763
|
+
className="w-full border border-gray-300 dark:border-gray-600 py-3 rounded-lg font-semibold hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
|
764
|
+
>
|
|
765
|
+
Close
|
|
766
|
+
</button>
|
|
767
|
+
</div>
|
|
768
|
+
</motion.div>
|
|
769
|
+
)}
|
|
770
|
+
</AnimatePresence>
|
|
771
|
+
</div>
|
|
772
|
+
|
|
773
|
+
{/* Footer */}
|
|
774
|
+
<div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700 text-center">
|
|
775
|
+
<p className="text-xs text-gray-500">
|
|
776
|
+
Powered by <span className="font-semibold text-[#7042D2]">Coinley</span>
|
|
777
|
+
</p>
|
|
778
|
+
</div>
|
|
779
|
+
</motion.div>
|
|
780
|
+
</div>
|
|
781
|
+
|
|
782
|
+
<WalletModal />
|
|
783
|
+
</CoinleyWalletProvider>
|
|
784
|
+
);
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
export default CoinleyPayment;
|