Integrate solana token payments

This commit is contained in:
Shreerang Kale
2025-07-18 12:26:54 +05:30
parent 765fe3d49a
commit 982232545a
15 changed files with 1684 additions and 153 deletions
+51 -23
View File
@@ -2,26 +2,27 @@
import { useState } from 'react';
import { sendAtomPayment } from '@/services/keplr';
interface PaymentModalProps {
isOpen: boolean;
onClose: () => void;
url: string;
onPaymentComplete: (txHash: string) => void;
}
import { sendGorPayment } from '@/services/solana';
import { PaymentModalProps } from '@/types';
import { getSolanaConfig, GOR_PAYMENT_AMOUNT } from '@/config';
export default function PaymentModal({
isOpen,
onClose,
url,
paymentType,
onPaymentComplete,
}: PaymentModalProps) {
const [amount, setAmount] = useState('0.01');
const [amount, setAmount] = useState(paymentType === 'ATOM' ? '0.01' : GOR_PAYMENT_AMOUNT.toString());
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
// Get recipient address from environment variables
const recipientAddress = process.env.NEXT_PUBLIC_RECIPIENT_ADDRESS || 'cosmos1yourrealaddress';
// Get recipient addresses from environment variables
const atomRecipientAddress = process.env.NEXT_PUBLIC_RECIPIENT_ADDRESS || 'cosmos1yourrealaddress';
const gorRecipientAddress = paymentType === 'GOR' ?
(process.env.NEXT_PUBLIC_GOR_RECIPIENT_ADDRESS || 'solana_recipient_address') : '';
const recipientAddress = paymentType === 'ATOM' ? atomRecipientAddress : gorRecipientAddress;
// Validate amount on change
const handleAmountChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -46,12 +47,30 @@ export default function PaymentModal({
setError('');
try {
const result = await sendAtomPayment(recipientAddress, amount);
if (result.status === 'success' && result.hash) {
onPaymentComplete(result.hash);
} else {
setError(result.message || 'Payment failed. Please try again.');
if (paymentType === 'ATOM') {
const result = await sendAtomPayment(recipientAddress, amount);
if (result.status === 'success' && result.hash) {
onPaymentComplete(result.hash);
} else {
setError(result.message || 'ATOM payment failed. Please try again.');
}
} else if (paymentType === 'GOR') {
// For GOR payments, we need wallet info from parent component
// This is a simplified approach - in a real implementation, you'd pass wallet state
const walletInfo = (window as any).solanaWalletInfo;
if (!walletInfo || !walletInfo.publicKey || !walletInfo.walletType) {
setError('Solana wallet not connected. Please connect your wallet first.');
return;
}
const result = await sendGorPayment(walletInfo.publicKey, walletInfo.walletType);
if (result.success && result.transactionSignature) {
onPaymentComplete(result.transactionSignature);
} else {
setError(result.error || 'GOR payment failed. Please try again.');
}
}
} catch (error) {
setError(error instanceof Error ? error.message : 'Payment failed. Please try again.');
@@ -67,7 +86,9 @@ export default function PaymentModal({
<div className="max-w-md w-full rounded-xl shadow-xl animate-appear"
style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}>
<div className="p-6 border-b" style={{ borderColor: 'var(--card-border)' }}>
<h2 className="text-xl font-semibold" style={{ color: 'var(--foreground)' }}>Complete Payment</h2>
<h2 className="text-xl font-semibold" style={{ color: 'var(--foreground)' }}>
Complete {paymentType} Payment
</h2>
</div>
<div className="p-6 space-y-6">
@@ -87,27 +108,34 @@ export default function PaymentModal({
<div>
<label htmlFor="amount" className="block text-sm font-medium mb-2" style={{ color: 'var(--foreground)' }}>
Amount (ATOM)
Amount ({paymentType})
</label>
<div className="relative">
<input
id="amount"
type="number"
min="0.01"
step="0.01"
min={paymentType === 'ATOM' ? "0.01" : "1"}
step={paymentType === 'ATOM' ? "0.01" : "1"}
value={amount}
onChange={handleAmountChange}
disabled={paymentType === 'GOR'} // Fixed amount for GOR
className="w-full p-3 pr-12 rounded-md"
style={{
background: 'var(--card-bg)',
border: '1px solid var(--input-border)',
color: 'var(--foreground)'
color: 'var(--foreground)',
opacity: paymentType === 'GOR' ? '0.7' : '1'
}}
/>
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
<span className="text-sm font-medium" style={{ color: 'var(--muted)' }}>ATOM</span>
<span className="text-sm font-medium" style={{ color: 'var(--muted)' }}>{paymentType}</span>
</div>
</div>
{paymentType === 'GOR' && (
<p className="text-xs mt-1" style={{ color: 'var(--muted)' }}>
Fixed amount required for deployment
</p>
)}
</div>
{error && (
@@ -146,7 +174,7 @@ export default function PaymentModal({
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
)}
{loading ? 'Processing...' : 'Pay with Keplr'}
{loading ? 'Processing...' : `Pay with ${paymentType === 'ATOM' ? 'Keplr' : 'Solana Wallet'}`}
</button>
</div>
</div>
+148
View File
@@ -0,0 +1,148 @@
'use client';
import { useState, useEffect } from 'react';
import { connectSolanaWallet, disconnectSolanaWallet, checkSolanaWalletConnection } from '@/services/solana';
import { SolanaWalletType, SolanaWalletState } from '@/types';
interface SolanaConnectProps {
onConnect: (walletState: SolanaWalletState) => void;
}
export default function SolanaConnect({ onConnect }: SolanaConnectProps) {
const [connecting, setConnecting] = useState(false);
const [walletState, setWalletState] = useState<SolanaWalletState>({
connected: false,
publicKey: null,
walletType: null
});
const handleConnect = async (walletType: SolanaWalletType) => {
setConnecting(true);
try {
const newWalletState = await connectSolanaWallet(walletType);
setWalletState(newWalletState);
onConnect(newWalletState);
} catch (error) {
console.error('Failed to connect to Solana wallet:', error);
alert(error instanceof Error ? error.message : 'Failed to connect wallet');
} finally {
setConnecting(false);
}
};
const handleDisconnect = async () => {
if (walletState.walletType) {
try {
await disconnectSolanaWallet(walletState.walletType);
const disconnectedState = {
connected: false,
publicKey: null,
walletType: null
};
setWalletState(disconnectedState);
onConnect(disconnectedState);
} catch (error) {
console.error('Failed to disconnect wallet:', error);
}
}
};
useEffect(() => {
// Check for auto-connection on page load
const checkConnection = () => {
if (typeof window !== 'undefined') {
// Check Phantom
if (window.phantom?.solana && checkSolanaWalletConnection('phantom')) {
handleConnect('phantom');
return;
}
// Check Solflare
if (window.solflare && checkSolanaWalletConnection('solflare')) {
handleConnect('solflare');
return;
}
}
};
checkConnection();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className="flex flex-col items-center p-4 rounded-lg">
{walletState.connected ? (
<div className="flex flex-col items-center w-full">
<div className="flex items-center mb-2">
<span className="w-3 h-3 rounded-full mr-2" style={{ backgroundColor: 'var(--success)' }}></span>
<p className="font-medium" style={{ color: 'var(--success)' }}>
Connected ({walletState.walletType})
</p>
</div>
<div className="w-full p-3 rounded-md mb-3" style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}>
<p className="text-sm font-mono break-all text-center">{walletState.publicKey}</p>
</div>
<button
onClick={handleDisconnect}
className="px-4 py-2 rounded-md text-sm transition-colors"
style={{
backgroundColor: 'var(--muted)',
color: 'var(--foreground)',
border: '1px solid var(--input-border)'
}}
>
Disconnect
</button>
</div>
) : (
<div className="w-full space-y-3">
<button
onClick={() => handleConnect('phantom')}
disabled={connecting || !window.phantom?.solana}
className="px-6 py-3 rounded-md w-full transition-colors disabled:opacity-50"
style={{
backgroundColor: connecting ? 'var(--muted)' : 'var(--primary)',
color: 'var(--primary-foreground)',
}}
>
<div className="flex items-center justify-center">
{connecting && (
<svg className="animate-spin -ml-1 mr-3 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
)}
{connecting ? 'Connecting...' : 'Connect Phantom Wallet'}
</div>
</button>
<button
onClick={() => handleConnect('solflare')}
disabled={connecting || !window.solflare}
className="px-6 py-3 rounded-md w-full transition-colors disabled:opacity-50"
style={{
backgroundColor: connecting ? 'var(--muted)' : 'var(--primary)',
color: 'var(--primary-foreground)',
}}
>
<div className="flex items-center justify-center">
{connecting && (
<svg className="animate-spin -ml-1 mr-3 h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
)}
{connecting ? 'Connecting...' : 'Connect Solflare Wallet'}
</div>
</button>
{!window.phantom?.solana && !window.solflare && (
<p className="text-sm text-center" style={{ color: 'var(--muted)' }}>
Please install Phantom or Solflare wallet extension
</p>
)}
</div>
)}
</div>
);
}