Add payment method context to choose between native gorbagana and solana chain tokens

This commit is contained in:
Shreerang Kale 2025-07-23 17:32:12 +05:30
parent 9995e76e87
commit b47103bafa
7 changed files with 332 additions and 178 deletions

View File

@ -1,8 +1,8 @@
# Client-side environment variables (must be prefixed with NEXT_PUBLIC_) # Client-side environment variables (must be prefixed with NEXT_PUBLIC_)
# Solana Token Payment Configuration # Solana Payment Configuration
# TODO: Use different RPC URL or use browser wallet
NEXT_PUBLIC_SOLANA_RPC_URL=https://skilled-prettiest-seed.solana-mainnet.quiknode.pro/eeecfebd04e345f69f1900cc3483cbbfea02a158 NEXT_PUBLIC_SOLANA_RPC_URL=https://skilled-prettiest-seed.solana-mainnet.quiknode.pro/eeecfebd04e345f69f1900cc3483cbbfea02a158
NEXT_PUBLIC_SOLANA_SOL_RPC_URL=https://rpc.gorbagana.wtf
NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS=71Jvq4Epe2FCJ7JFSF7jLXdNk1Wy4Bhqd9iL6bEFELvg NEXT_PUBLIC_SOLANA_TOKEN_MINT_ADDRESS=71Jvq4Epe2FCJ7JFSF7jLXdNk1Wy4Bhqd9iL6bEFELvg
# Multisig address # Multisig address

View File

@ -15,7 +15,10 @@ import { PaymentMethod, SOL_PAYMENT_AMOUNT_LAMPORTS } from '@/constants/payments
import { getRecipientAddress } from '@/services/solana'; import { getRecipientAddress } from '@/services/solana';
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required'); assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
assert(process.env.NEXT_PUBLIC_SOLANA_SOL_RPC_URL, 'SOLANA_SOL_RPC_URL is required');
const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL; const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
const SOLANA_SOL_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_SOL_RPC_URL;
// Allow 20% slippage due to price fluctuations // Allow 20% slippage due to price fluctuations
const ALLOWED_SLIPPAGE_FACTOR = 0.2 const ALLOWED_SLIPPAGE_FACTOR = 0.2
@ -136,16 +139,19 @@ export const registryTransactionWithRetry = async (
throw lastError; throw lastError;
}; };
let connection: Connection; // Helper function to get the appropriate connection based on payment method
const getConnection = (paymentMethod: PaymentMethod): Connection => {
if (paymentMethod === 'sol' && SOLANA_SOL_RPC_URL) {
return new Connection(SOLANA_SOL_RPC_URL);
}
return new Connection(SOLANA_RPC_URL);
};
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
try { try {
if (!connection) {
connection = new Connection(SOLANA_RPC_URL);
}
// First check if the request body is valid JSON // First check if the request body is valid JSON
let url, txHash, senderPublicKey, paymentMethod; let url, txHash, senderPublicKey, paymentMethod;
let connection: Connection;
try { try {
const body = await request.json(); const body = await request.json();
@ -153,6 +159,9 @@ export async function POST(request: NextRequest) {
txHash = body.txHash; txHash = body.txHash;
paymentMethod = body.paymentMethod as PaymentMethod; paymentMethod = body.paymentMethod as PaymentMethod;
// Get the appropriate connection based on payment method
connection = getConnection(paymentMethod);
const tx = await connection.getParsedTransaction(txHash, 'confirmed'); const tx = await connection.getParsedTransaction(txHash, 'confirmed');
if (!tx) { if (!tx) {
console.error("Transaction not found."); console.error("Transaction not found.");

View File

@ -4,6 +4,7 @@ import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
import ErrorBoundaryWrapper from "../components/ErrorBoundaryWrapper"; import ErrorBoundaryWrapper from "../components/ErrorBoundaryWrapper";
import WalletProviders from "../components/WalletProviders"; import WalletProviders from "../components/WalletProviders";
import { PaymentMethodProvider } from "../contexts/PaymentMethodContext";
const geistSans = Geist({ const geistSans = Geist({
@ -32,9 +33,11 @@ export default function RootLayout({
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`${geistSans.variable} ${geistMono.variable} antialiased`}
> >
<ErrorBoundaryWrapper /> <ErrorBoundaryWrapper />
<WalletProviders> <PaymentMethodProvider>
{children} <WalletProviders>
</WalletProviders> {children}
</WalletProviders>
</PaymentMethodProvider>
</body> </body>
</html> </html>
); );

View File

@ -7,14 +7,17 @@ import { WalletMultiButton } from '@solana/wallet-adapter-react-ui';
import URLForm from '@/components/URLForm'; import URLForm from '@/components/URLForm';
import StatusDisplay from '@/components/StatusDisplay'; import StatusDisplay from '@/components/StatusDisplay';
import { createApplicationDeploymentRequest } from '@/services/registry'; import { createApplicationDeploymentRequest } from '@/services/registry';
import { PaymentMethod } from '@/constants/payments'; import { PaymentMethod, PAYMENT_METHOD_LABELS } from '@/constants/payments';
import { useWallet } from '@solana/wallet-adapter-react'; import { useWallet } from '@solana/wallet-adapter-react';
import { usePaymentMethod } from '@/contexts/PaymentMethodContext';
// Dynamically import components to avoid SSR issues with browser APIs // Dynamically import components to avoid SSR issues with browser APIs
const PaymentModal = dynamic(() => import('@/components/PaymentModal'), { ssr: false }); const PaymentModal = dynamic(() => import('@/components/PaymentModal'), { ssr: false });
export default function Home() { export default function Home() {
const { wallet, connected, publicKey } = useWallet(); const { wallet, connected, publicKey } = useWallet();
const { selectedPaymentMethod, setSelectedPaymentMethod } = usePaymentMethod();
const [url, setUrl] = useState<string | null>(null); const [url, setUrl] = useState<string | null>(null);
const [showPaymentModal, setShowPaymentModal] = useState(false); const [showPaymentModal, setShowPaymentModal] = useState(false);
const [status, setStatus] = useState<'idle' | 'creating' | 'success' | 'error'>('idle'); const [status, setStatus] = useState<'idle' | 'creating' | 'success' | 'error'>('idle');
@ -32,6 +35,20 @@ export default function Home() {
setShowPaymentModal(true); setShowPaymentModal(true);
}; };
// Helper function to check if current wallet is compatible with selected payment method
const isWalletCompatible = () => {
if (!selectedPaymentMethod || !wallet) return false;
const walletName = wallet.adapter.name.toLowerCase();
const isBackpack = walletName.includes('backpack');
if (selectedPaymentMethod === 'sol') {
return isBackpack; // Only Backpack for SOL
} else {
return !isBackpack; // Only non-Backpack wallets for SPL tokens
}
};
const handlePaymentComplete = useCallback(async (hash: string, paymentMethod: PaymentMethod) => { const handlePaymentComplete = useCallback(async (hash: string, paymentMethod: PaymentMethod) => {
if (!publicKey || !url) { if (!publicKey || !url) {
return return
@ -85,62 +102,143 @@ export default function Home() {
Deploy Frontends with {process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} + Laconic Deploy Frontends with {process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} + Laconic
</h1> </h1>
{/* Step 1: Payment Method Selection */}
<div className="mb-10 p-6 rounded-lg" style={{ background: 'var(--muted-light)', borderLeft: '4px solid var(--primary)' }}> <div className="mb-10 p-6 rounded-lg" style={{ background: 'var(--muted-light)', borderLeft: '4px solid var(--primary)' }}>
<h2 className="text-lg font-semibold mb-4 flex items-center"> <h2 className="text-lg font-semibold mb-4 flex items-center">
<span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold" <span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold"
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>1</span> style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>1</span>
Connect Your Wallet Choose Payment Method
</h2> </h2>
<div className="text-center"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<p className="mb-4" style={{ color: 'var(--muted-foreground)' }}> <button
Payment methods: <span className="font-semibold" style={{ color: 'var(--foreground)' }}> onClick={() => setSelectedPaymentMethod('sol')}
SOL or {process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} (Solana) className={`p-4 rounded-lg border-2 transition-colors ${
</span> selectedPaymentMethod === 'sol' ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-gray-400'
</p> }`}
{connected && publicKey ? ( style={{
<div className="flex flex-col items-center space-y-3"> backgroundColor: selectedPaymentMethod === 'sol' ? 'var(--accent)' : 'var(--card-bg)',
<div className="flex items-center"> borderColor: selectedPaymentMethod === 'sol' ? 'var(--primary)' : 'var(--card-border)'
<span className="w-3 h-3 rounded-full mr-2" style={{ backgroundColor: 'var(--success)' }}></span> }}
<p className="font-medium" style={{ color: 'var(--success)' }}> >
Connected ({wallet?.adapter.name}) <div className="text-left">
</p> <h3 className="font-semibold text-lg mb-2">Native SOL</h3>
</div> <p className="text-sm" style={{ color: 'var(--muted-foreground)' }}>
<div className="w-full p-3 rounded-md" style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}> Pay 0.01 SOL (~$2-5 USD)
<p className="text-sm font-mono break-all text-center">{publicKey.toBase58()}</p> </p>
</div> <p className="text-xs mt-1" style={{ color: 'var(--muted-foreground)' }}>
<WalletMultiButton Compatible with: Backpack
className="!px-4 !py-2 !rounded-md !text-sm !transition-colors" </p>
style={{
backgroundColor: 'var(--muted)',
color: 'var(--foreground)',
border: '1px solid var(--input-border)'
}}
/>
</div> </div>
) : ( </button>
<WalletMultiButton <button
className="!px-6 !py-3 !rounded-md !w-full !transition-colors" onClick={() => setSelectedPaymentMethod('spl-token')}
style={{ className={`p-4 rounded-lg border-2 transition-colors ${
backgroundColor: 'var(--primary)', selectedPaymentMethod === 'spl-token' ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-gray-400'
color: 'var(--primary-foreground)', }`}
}} style={{
/> backgroundColor: selectedPaymentMethod === 'spl-token' ? 'var(--accent)' : 'var(--card-bg)',
)} borderColor: selectedPaymentMethod === 'spl-token' ? 'var(--primary)' : 'var(--card-border)'
}}
>
<div className="text-left">
<h3 className="font-semibold text-lg mb-2">{process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} Token</h3>
<p className="text-sm" style={{ color: 'var(--muted-foreground)' }}>
Pay ${process.env.NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT_USD} worth
</p>
<p className="text-xs mt-1" style={{ color: 'var(--muted-foreground)' }}>
Compatible with: Phantom, Solflare
</p>
</div>
</button>
</div> </div>
</div> </div>
<div className="mb-8 p-6 rounded-lg" style={{
{/* Step 2: Wallet Connection */}
<div className="mb-10 p-6 rounded-lg" style={{
background: 'var(--muted-light)', background: 'var(--muted-light)',
borderLeft: '4px solid var(--primary)', borderLeft: '4px solid var(--primary)',
opacity: connected ? '1' : '0.6' opacity: selectedPaymentMethod ? '1' : '0.6'
}}> }}>
<h2 className="text-lg font-semibold mb-4 flex items-center"> <h2 className="text-lg font-semibold mb-4 flex items-center">
<span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold" <span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold"
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>2</span> style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>2</span>
Connect Compatible Wallet
</h2>
{!selectedPaymentMethod ? (
<p className="text-center" style={{ color: 'var(--muted-foreground)' }}>
Please select a payment method first
</p>
) : (
<div className="text-center">
<p className="mb-4" style={{ color: 'var(--muted-foreground)' }}>
Selected: <span className="font-semibold" style={{ color: 'var(--foreground)' }}>
{PAYMENT_METHOD_LABELS[selectedPaymentMethod]}
</span>
</p>
{connected && publicKey ? (
<div className="flex flex-col items-center space-y-3">
<div className="flex items-center">
<span className="w-3 h-3 rounded-full mr-2" style={{
backgroundColor: isWalletCompatible() ? 'var(--success)' : 'var(--destructive)'
}}></span>
<p className="font-medium" style={{
color: isWalletCompatible() ? 'var(--success)' : 'var(--destructive)'
}}>
{isWalletCompatible() ? 'Compatible' : 'Incompatible'} Wallet ({wallet?.adapter.name})
</p>
</div>
{!isWalletCompatible() && (
<p className="text-sm" style={{ color: 'var(--destructive)' }}>
This wallet is not compatible with {PAYMENT_METHOD_LABELS[selectedPaymentMethod]} payments.
Please select a different wallet or payment method.
</p>
)}
<div className="w-full p-3 rounded-md" style={{ background: 'var(--card-bg)', border: '1px solid var(--card-border)' }}>
<p className="text-sm font-mono break-all text-center">{publicKey.toBase58()}</p>
</div>
<WalletMultiButton
className="!px-4 !py-2 !rounded-md !text-sm !transition-colors"
style={{
backgroundColor: 'var(--muted)',
color: 'var(--foreground)',
border: '1px solid var(--input-border)'
}}
/>
</div>
) : (
<div>
<p className="mb-4 text-sm" style={{ color: 'var(--muted-foreground)' }}>
{selectedPaymentMethod === 'sol'
? 'Only Backpack wallet supports SOL payments'
: 'Phantom and Solflare wallets support SPL token payments'
}
</p>
<WalletMultiButton
className="!px-6 !py-3 !rounded-md !w-full !transition-colors"
style={{
backgroundColor: 'var(--primary)',
color: 'var(--primary-foreground)',
}}
/>
</div>
)}
</div>
)}
</div>
{/* Step 3: URL Input */}
<div className="mb-8 p-6 rounded-lg" style={{
background: 'var(--muted-light)',
borderLeft: '4px solid var(--primary)',
opacity: (connected && isWalletCompatible()) ? '1' : '0.6'
}}>
<h2 className="text-lg font-semibold mb-4 flex items-center">
<span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold"
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>3</span>
Enter URL to Deploy Enter URL to Deploy
</h2> </h2>
<URLForm <URLForm
onSubmit={handleUrlSubmit} onSubmit={handleUrlSubmit}
disabled={!connected || status === 'creating'} disabled={!connected || !isWalletCompatible() || status === 'creating'}
/> />
</div> </div>
@ -148,7 +246,7 @@ export default function Home() {
<div className="p-6 rounded-lg" style={{ background: 'var(--muted-light)', borderLeft: '4px solid var(--primary)' }}> <div className="p-6 rounded-lg" style={{ background: 'var(--muted-light)', borderLeft: '4px solid var(--primary)' }}>
<h2 className="text-lg font-semibold mb-4 flex items-center"> <h2 className="text-lg font-semibold mb-4 flex items-center">
<span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold" <span className="inline-flex items-center justify-center mr-3 w-7 h-7 rounded-full text-sm font-bold"
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>3</span> style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>4</span>
Deployment Status Deployment Status
</h2> </h2>
<StatusDisplay <StatusDisplay
@ -166,7 +264,7 @@ export default function Home() {
)} )}
</div> </div>
{showPaymentModal && url && connected && publicKey && ( {showPaymentModal && url && connected && publicKey && selectedPaymentMethod && (
<PaymentModal <PaymentModal
isOpen={showPaymentModal} isOpen={showPaymentModal}
onClose={handleClosePaymentModal} onClose={handleClosePaymentModal}

View File

@ -9,10 +9,12 @@ import { useWallet } from '@solana/wallet-adapter-react';
import { sendSolanaPayment, getRecipientAddress } from '@/services/solana'; import { sendSolanaPayment, getRecipientAddress } from '@/services/solana';
import { getRequiredTokenInfo } from '@/services/jupiter-price'; import { getRequiredTokenInfo } from '@/services/jupiter-price';
import { PaymentModalProps, PaymentRequest } from '@/types'; import { PaymentModalProps, PaymentRequest } from '@/types';
import { PaymentMethod, PAYMENT_METHOD_LABELS, SOL_PAYMENT_AMOUNT_LAMPORTS } from '@/constants/payments'; import { PAYMENT_METHOD_LABELS, SOL_PAYMENT_AMOUNT_LAMPORTS } from '@/constants/payments';
import { usePaymentMethod } from '@/contexts/PaymentMethodContext';
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required'); assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL; const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
const SOLANA_SOL_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_SOL_RPC_URL;
export default function PaymentModal({ export default function PaymentModal({
isOpen, isOpen,
@ -22,7 +24,8 @@ export default function PaymentModal({
}: PaymentModalProps) { }: PaymentModalProps) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<PaymentMethod | null>(null); const { selectedPaymentMethod } = usePaymentMethod();
const paymentMethod = selectedPaymentMethod;
const [tokenAmount, setTokenAmount] = useState<number>(0); const [tokenAmount, setTokenAmount] = useState<number>(0);
const [tokenDecimals, setTokenDecimals] = useState<number>(6); // Default fallback const [tokenDecimals, setTokenDecimals] = useState<number>(6); // Default fallback
const [loadingPrice, setLoadingPrice] = useState(false); const [loadingPrice, setLoadingPrice] = useState(false);
@ -30,6 +33,10 @@ export default function PaymentModal({
const { wallet, publicKey } = useWallet(); const { wallet, publicKey } = useWallet();
const directConnection = useMemo(() => new Connection(SOLANA_RPC_URL), []); const directConnection = useMemo(() => new Connection(SOLANA_RPC_URL), []);
const solConnection = useMemo(() =>
SOLANA_SOL_RPC_URL ? new Connection(SOLANA_SOL_RPC_URL) : directConnection,
[directConnection]
);
// Get configuration from environment variables // Get configuration from environment variables
const targetUsdAmount = parseFloat(process.env.NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT_USD!); const targetUsdAmount = parseFloat(process.env.NEXT_PUBLIC_SOLANA_PAYMENT_AMOUNT_USD!);
@ -38,7 +45,7 @@ export default function PaymentModal({
// Fetch token amount based on USD price when SPL token method is selected // Fetch token amount based on USD price when SPL token method is selected
useEffect(() => { useEffect(() => {
if (!isOpen || selectedPaymentMethod !== 'spl-token') { if (!isOpen || paymentMethod !== 'spl-token') {
setLoadingPrice(false); setLoadingPrice(false);
return; return;
} }
@ -60,35 +67,30 @@ export default function PaymentModal({
}; };
fetchTokenAmount(); fetchTokenAmount();
}, [isOpen, selectedPaymentMethod, targetUsdAmount, mintAddress]); }, [isOpen, paymentMethod, targetUsdAmount, mintAddress]);
// Reset state when modal opens // Initialize payment method when modal opens
useEffect(() => { useEffect(() => {
if (isOpen) { if (isOpen) {
setSelectedPaymentMethod(null);
setError(''); setError('');
setTokenAmount(0);
setLoadingPrice(false); setLoadingPrice(false);
}
}, [isOpen]);
const handlePaymentMethodChange = (method: PaymentMethod) => { // Set tokenAmount for SOL payments to maintain consistency
setSelectedPaymentMethod(method); if (paymentMethod === 'sol') {
setError(''); setTokenAmount(SOL_PAYMENT_AMOUNT_LAMPORTS);
} else {
// Set tokenAmount for SOL payments to maintain consistency setTokenAmount(0);
if (method === 'sol') { }
setTokenAmount(SOL_PAYMENT_AMOUNT_LAMPORTS);
} }
}; }, [isOpen, paymentMethod]);
const handlePayment = useCallback(async () => { const handlePayment = useCallback(async () => {
if (!selectedPaymentMethod) { if (!paymentMethod) {
setError('Please select a payment method.');
return; return;
} }
if (tokenAmount === 0 || (selectedPaymentMethod === 'spl-token' && loadingPrice)) { if (tokenAmount === 0 || (paymentMethod === 'spl-token' && loadingPrice)) {
setError('Payment amount not ready. Please wait.'); setError('Payment amount not ready. Please wait.');
return; return;
} }
@ -103,20 +105,23 @@ export default function PaymentModal({
try { try {
const paymentRequest: PaymentRequest = { const paymentRequest: PaymentRequest = {
paymentMethod: selectedPaymentMethod, paymentMethod: paymentMethod,
amount: tokenAmount, amount: tokenAmount,
recipientAddress: getRecipientAddress(selectedPaymentMethod) recipientAddress: getRecipientAddress(paymentMethod)
}; };
// Use different RPC connection based on payment method
const connectionToUse = paymentMethod === 'sol' ? solConnection : directConnection;
const result = await sendSolanaPayment( const result = await sendSolanaPayment(
wallet.adapter, wallet.adapter,
directConnection, connectionToUse,
publicKey!.toBase58(), publicKey!.toBase58(),
paymentRequest paymentRequest
); );
if (result.success && result.transactionSignature) { if (result.success && result.transactionSignature) {
onPaymentComplete(result.transactionSignature, selectedPaymentMethod); onPaymentComplete(result.transactionSignature, paymentMethod);
} else { } else {
setError(result.error || 'Payment failed. Please try again.'); setError(result.error || 'Payment failed. Please try again.');
} }
@ -125,12 +130,10 @@ export default function PaymentModal({
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [selectedPaymentMethod, tokenAmount, loadingPrice, wallet, directConnection, publicKey, onPaymentComplete]); }, [paymentMethod, tokenAmount, loadingPrice, wallet, directConnection, solConnection, publicKey, onPaymentComplete]);
const getPaymentAmountDisplay = () => { const getPaymentAmountDisplay = () => {
if (!selectedPaymentMethod) return ''; switch (paymentMethod) {
switch (selectedPaymentMethod) {
case 'sol': case 'sol':
return `${SOL_PAYMENT_AMOUNT_LAMPORTS / LAMPORTS_PER_SOL} SOL`; return `${SOL_PAYMENT_AMOUNT_LAMPORTS / LAMPORTS_PER_SOL} SOL`;
case 'spl-token': case 'spl-token':
@ -141,7 +144,7 @@ export default function PaymentModal({
} }
}; };
if (!isOpen) return null; if (!isOpen || !paymentMethod) return null;
return ( return (
<div className="fixed inset-0 flex items-center justify-center p-4 z-50" style={{ background: 'rgba(15, 23, 42, 0.75)' }}> <div className="fixed inset-0 flex items-center justify-center p-4 z-50" style={{ background: 'rgba(15, 23, 42, 0.75)' }}>
@ -161,111 +164,93 @@ export default function PaymentModal({
</div> </div>
</div> </div>
{/* Payment Method Selection */} {/* Payment Method Display */}
<div> <div>
<label className="block text-sm font-medium mb-3" style={{ color: 'var(--foreground)' }}> <label className="block text-sm font-medium mb-3" style={{ color: 'var(--foreground)' }}>
Select Payment Method * Payment Method
</label> </label>
<div className="space-y-2"> <div className="p-3 rounded-md" style={{ background: 'var(--muted-light)', color: 'var(--foreground)' }}>
{Object.entries(PAYMENT_METHOD_LABELS).map(([method, label]) => ( <p className="text-sm font-semibold">
<label key={method} className="flex items-center p-3 rounded-md cursor-pointer border" {PAYMENT_METHOD_LABELS[paymentMethod]}
style={{ </p>
borderColor: selectedPaymentMethod === method ? 'var(--primary)' : 'var(--input-border)',
backgroundColor: selectedPaymentMethod === method ? 'var(--primary-light)' : 'var(--card-bg)'
}}>
<input
type="radio"
name="paymentMethod"
value={method}
checked={selectedPaymentMethod === method}
onChange={() => handlePaymentMethodChange(method as PaymentMethod)}
className="mr-3"
/>
<span style={{ color: 'var(--foreground)' }}>{label}</span>
</label>
))}
</div> </div>
</div> </div>
{/* Payment Details - Only show when method is selected */} {/* Payment Details */}
{selectedPaymentMethod && ( <div>
<> <p className="text-sm mb-2 font-medium" style={{ color: 'var(--muted)' }}>Recipient Address:</p>
<div> <div className="p-3 rounded-md overflow-hidden" style={{ background: 'var(--muted-light)', color: 'var(--foreground)' }}>
<p className="text-sm mb-2 font-medium" style={{ color: 'var(--muted)' }}>Recipient Address:</p> <code className="text-sm font-mono break-all block">{getRecipientAddress(paymentMethod)}</code>
<div className="p-3 rounded-md overflow-hidden" style={{ background: 'var(--muted-light)', color: 'var(--foreground)' }}> </div>
<code className="text-sm font-mono break-all block">{getRecipientAddress(selectedPaymentMethod)}</code> </div>
<div>
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--foreground)' }}>
Payment Amount
</label>
<div className="space-y-3">
<div className="relative">
<input
type="text"
value={targetUsdAmount}
disabled={true}
className="w-full p-3 pr-12 rounded-md"
style={{
background: 'var(--card-bg)',
border: '1px solid var(--input-border)',
color: 'var(--foreground)',
opacity: '0.7'
}}
readOnly
/>
<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)' }}>USD</span>
</div> </div>
</div> </div>
<div> <div className="relative">
<label className="block text-sm font-medium mb-2" style={{ color: 'var(--foreground)' }}> {(paymentMethod === 'spl-token' && loadingPrice) ? (
Payment Amount <div className="w-full p-3 rounded-md flex items-center" style={{
</label> background: 'var(--muted-light)',
<div className="space-y-3"> border: '1px solid var(--input-border)',
<div className="relative"> color: 'var(--muted)'
<input }}>
type="text" <svg className="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
value={targetUsdAmount} <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
disabled={true} <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>
className="w-full p-3 pr-12 rounded-md" </svg>
style={{ Fetching token amount...
background: 'var(--card-bg)',
border: '1px solid var(--input-border)',
color: 'var(--foreground)',
opacity: '0.7'
}}
readOnly
/>
<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)' }}>USD</span>
</div>
</div> </div>
) : (
<div className="relative"> <input
{(selectedPaymentMethod === 'spl-token' && loadingPrice) ? ( type="text"
<div className="w-full p-3 rounded-md flex items-center" style={{ value={getPaymentAmountDisplay()}
background: 'var(--muted-light)', disabled={true}
border: '1px solid var(--input-border)', className="w-full p-3 pr-20 rounded-md"
color: 'var(--muted)' style={{
}}> background: 'var(--card-bg)',
<svg className="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> border: '1px solid var(--input-border)',
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle> color: 'var(--foreground)',
<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> opacity: '0.7'
</svg> }}
Fetching token amount... readOnly
</div> />
) : ( )}
<input {!(paymentMethod === 'spl-token' && loadingPrice) && (
type="text" <div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
value={getPaymentAmountDisplay()} <span className="text-sm font-medium" style={{ color: 'var(--muted)' }}>
disabled={true} {paymentMethod === 'sol' ? 'SOL' : tokenSymbol}
className="w-full p-3 pr-20 rounded-md" </span>
style={{
background: 'var(--card-bg)',
border: '1px solid var(--input-border)',
color: 'var(--foreground)',
opacity: '0.7'
}}
readOnly
/>
)}
{!(selectedPaymentMethod === 'spl-token' && loadingPrice) && (
<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)' }}>
{selectedPaymentMethod === 'sol' ? 'SOL' : tokenSymbol}
</span>
</div>
)}
</div> </div>
</div>
{selectedPaymentMethod === 'spl-token' && (
<p className="text-xs mt-1" style={{ color: 'var(--muted)' }}>
Token information fetched from <a className='text-blue-400 underline' href={`https://jup.ag/tokens/${mintAddress}`} target="_blank" rel="noopener noreferrer">Jupiter</a>
</p>
)} )}
</div> </div>
</> </div>
)} {paymentMethod === 'spl-token' && (
<p className="text-xs mt-1" style={{ color: 'var(--muted)' }}>
Token information fetched from <a className='text-blue-400 underline' href={`https://jup.ag/tokens/${mintAddress}`} target="_blank" rel="noopener noreferrer">Jupiter</a>
</p>
)}
</div>
{error && ( {error && (
<div className="p-3 rounded-md text-sm" style={{ backgroundColor: 'var(--error-light)', color: 'var(--error)' }}> <div className="p-3 rounded-md text-sm" style={{ backgroundColor: 'var(--error-light)', color: 'var(--error)' }}>
@ -291,11 +276,11 @@ export default function PaymentModal({
onClick={handlePayment} onClick={handlePayment}
className="px-5 py-2 rounded-md flex items-center transition-colors" className="px-5 py-2 rounded-md flex items-center transition-colors"
style={{ style={{
backgroundColor: (loading || loadingPrice || !selectedPaymentMethod) ? 'var(--muted)' : 'var(--primary)', backgroundColor: (loading || loadingPrice) ? 'var(--muted)' : 'var(--primary)',
color: 'var(--primary-foreground)', color: 'var(--primary-foreground)',
opacity: (loading || loadingPrice || !selectedPaymentMethod) ? '0.8' : '1' opacity: (loading || loadingPrice) ? '0.8' : '1'
}} }}
disabled={loading || loadingPrice || !selectedPaymentMethod} disabled={loading || loadingPrice}
> >
{(loading || loadingPrice) && ( {(loading || loadingPrice) && (
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> <svg className="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
@ -303,8 +288,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> <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> </svg>
)} )}
{!selectedPaymentMethod ? 'Select Payment Method' : {loadingPrice ? 'Loading Price...' :
loadingPrice ? 'Loading Price...' :
loading ? 'Processing...' : loading ? 'Processing...' :
'Pay with Solana Wallet'} 'Pay with Solana Wallet'}
</button> </button>

View File

@ -10,6 +10,7 @@ import { SolflareWalletAdapter } from '@solana/wallet-adapter-solflare';
// Default styles that can be overridden by your app // Default styles that can be overridden by your app
import '@solana/wallet-adapter-react-ui/styles.css'; import '@solana/wallet-adapter-react-ui/styles.css';
import assert from 'assert'; import assert from 'assert';
import { usePaymentMethod } from '@/contexts/PaymentMethodContext';
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required'); assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL; const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL;
@ -19,12 +20,15 @@ interface WalletProvidersProps {
} }
export default function WalletProviders({ children }: WalletProvidersProps) { export default function WalletProviders({ children }: WalletProvidersProps) {
const { selectedPaymentMethod } = usePaymentMethod();
// Configure the Solana network endpoint // Configure the Solana network endpoint
const endpoint = useMemo(() => { const endpoint = useMemo(() => {
return SOLANA_RPC_URL; return SOLANA_RPC_URL;
}, []); }, []);
const wallets = useMemo( // All available wallet adapters
const allWallets = useMemo(
() => [ () => [
new PhantomWalletAdapter(), new PhantomWalletAdapter(),
new SolflareWalletAdapter(), new SolflareWalletAdapter(),
@ -33,6 +37,24 @@ export default function WalletProviders({ children }: WalletProvidersProps) {
[] []
); );
// Filter wallets based on selected payment method
const wallets = useMemo(() => {
if (!selectedPaymentMethod) {
// If no payment method selected, show all wallets
return allWallets;
}
return allWallets.filter(wallet => {
const isBackpack = wallet.name.toLowerCase().includes('backpack');
if (selectedPaymentMethod === 'sol') {
return isBackpack; // Only Backpack for SOL
} else {
return !isBackpack; // Only non-Backpack wallets for SPL tokens
}
});
}, [allWallets, selectedPaymentMethod]);
return ( return (
<ConnectionProvider endpoint={endpoint}> <ConnectionProvider endpoint={endpoint}>
<WalletProvider wallets={wallets} autoConnect> <WalletProvider wallets={wallets} autoConnect>

View File

@ -0,0 +1,38 @@
'use client';
import React, { createContext, useContext, useState, ReactNode } from 'react';
import { PaymentMethod } from '@/constants/payments';
interface PaymentMethodContextType {
selectedPaymentMethod: PaymentMethod | null;
setSelectedPaymentMethod: (method: PaymentMethod | null) => void;
}
const PaymentMethodContext = createContext<PaymentMethodContextType | undefined>(undefined);
interface PaymentMethodProviderProps {
children: ReactNode;
}
export function PaymentMethodProvider({ children }: PaymentMethodProviderProps) {
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState<PaymentMethod | null>(null);
return (
<PaymentMethodContext.Provider
value={{
selectedPaymentMethod,
setSelectedPaymentMethod
}}
>
{children}
</PaymentMethodContext.Provider>
);
}
export function usePaymentMethod() {
const context = useContext(PaymentMethodContext);
if (context === undefined) {
throw new Error('usePaymentMethod must be used within a PaymentMethodProvider');
}
return context;
}