From b47103bafa7f020dba18d8e0d670a3becd1cbb01 Mon Sep 17 00:00:00 2001 From: Shreerang Kale Date: Wed, 23 Jul 2025 17:32:12 +0530 Subject: [PATCH] Add payment method context to choose between native gorbagana and solana chain tokens --- .env.example | 4 +- src/app/api/registry/route.ts | 19 ++- src/app/layout.tsx | 9 +- src/app/page.tsx | 180 +++++++++++++++----- src/components/PaymentModal.tsx | 236 ++++++++++++-------------- src/components/WalletProviders.tsx | 24 ++- src/contexts/PaymentMethodContext.tsx | 38 +++++ 7 files changed, 332 insertions(+), 178 deletions(-) create mode 100644 src/contexts/PaymentMethodContext.tsx diff --git a/.env.example b/.env.example index a45a019..975175e 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,8 @@ # Client-side environment variables (must be prefixed with NEXT_PUBLIC_) -# Solana Token Payment Configuration -# TODO: Use different RPC URL or use browser wallet +# Solana Payment Configuration 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 # Multisig address diff --git a/src/app/api/registry/route.ts b/src/app/api/registry/route.ts index 5d82733..e5a7055 100644 --- a/src/app/api/registry/route.ts +++ b/src/app/api/registry/route.ts @@ -15,7 +15,10 @@ import { PaymentMethod, SOL_PAYMENT_AMOUNT_LAMPORTS } from '@/constants/payments import { getRecipientAddress } from '@/services/solana'; 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_SOL_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_SOL_RPC_URL; // Allow 20% slippage due to price fluctuations const ALLOWED_SLIPPAGE_FACTOR = 0.2 @@ -136,16 +139,19 @@ export const registryTransactionWithRetry = async ( 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) { try { - if (!connection) { - connection = new Connection(SOLANA_RPC_URL); - } - // First check if the request body is valid JSON let url, txHash, senderPublicKey, paymentMethod; + let connection: Connection; try { const body = await request.json(); @@ -153,6 +159,9 @@ export async function POST(request: NextRequest) { txHash = body.txHash; paymentMethod = body.paymentMethod as PaymentMethod; + // Get the appropriate connection based on payment method + connection = getConnection(paymentMethod); + const tx = await connection.getParsedTransaction(txHash, 'confirmed'); if (!tx) { console.error("Transaction not found."); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f6ddd85..b6af7db 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,7 @@ import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import ErrorBoundaryWrapper from "../components/ErrorBoundaryWrapper"; import WalletProviders from "../components/WalletProviders"; +import { PaymentMethodProvider } from "../contexts/PaymentMethodContext"; const geistSans = Geist({ @@ -32,9 +33,11 @@ export default function RootLayout({ className={`${geistSans.variable} ${geistMono.variable} antialiased`} > - - {children} - + + + {children} + + ); diff --git a/src/app/page.tsx b/src/app/page.tsx index bc066f5..a1fea19 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -7,14 +7,17 @@ import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; import URLForm from '@/components/URLForm'; import StatusDisplay from '@/components/StatusDisplay'; 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 { usePaymentMethod } from '@/contexts/PaymentMethodContext'; + // Dynamically import components to avoid SSR issues with browser APIs const PaymentModal = dynamic(() => import('@/components/PaymentModal'), { ssr: false }); export default function Home() { const { wallet, connected, publicKey } = useWallet(); + const { selectedPaymentMethod, setSelectedPaymentMethod } = usePaymentMethod(); const [url, setUrl] = useState(null); const [showPaymentModal, setShowPaymentModal] = useState(false); const [status, setStatus] = useState<'idle' | 'creating' | 'success' | 'error'>('idle'); @@ -32,6 +35,20 @@ export default function Home() { 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) => { if (!publicKey || !url) { return @@ -85,62 +102,143 @@ export default function Home() { Deploy Frontends with {process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} + Laconic + {/* Step 1: Payment Method Selection */}

1 - Connect Your Wallet + Choose Payment Method

-
-

- Payment methods: - SOL or {process.env.NEXT_PUBLIC_SOLANA_TOKEN_SYMBOL} (Solana) - -

- {connected && publicKey ? ( -
-
- -

- Connected ({wallet?.adapter.name}) -

-
-
-

{publicKey.toBase58()}

-
- +
+ +
-

2 + Connect Compatible Wallet +

+ {!selectedPaymentMethod ? ( +

+ Please select a payment method first +

+ ) : ( +
+

+ Selected: + {PAYMENT_METHOD_LABELS[selectedPaymentMethod]} + +

+ {connected && publicKey ? ( +
+
+ +

+ {isWalletCompatible() ? 'Compatible' : 'Incompatible'} Wallet ({wallet?.adapter.name}) +

+
+ {!isWalletCompatible() && ( +

+ This wallet is not compatible with {PAYMENT_METHOD_LABELS[selectedPaymentMethod]} payments. + Please select a different wallet or payment method. +

+ )} +
+

{publicKey.toBase58()}

+
+ +
+ ) : ( +
+

+ {selectedPaymentMethod === 'sol' + ? 'Only Backpack wallet supports SOL payments' + : 'Phantom and Solflare wallets support SPL token payments' + } +

+ +
+ )} +
+ )} +
+ {/* Step 3: URL Input */} +
+

+ 3 Enter URL to Deploy

@@ -148,7 +246,7 @@ export default function Home() {

3 + style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>4 Deployment Status

- {showPaymentModal && url && connected && publicKey && ( + {showPaymentModal && url && connected && publicKey && selectedPaymentMethod && ( (null); + const { selectedPaymentMethod } = usePaymentMethod(); + const paymentMethod = selectedPaymentMethod; const [tokenAmount, setTokenAmount] = useState(0); const [tokenDecimals, setTokenDecimals] = useState(6); // Default fallback const [loadingPrice, setLoadingPrice] = useState(false); @@ -30,6 +33,10 @@ export default function PaymentModal({ const { wallet, publicKey } = useWallet(); 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 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 useEffect(() => { - if (!isOpen || selectedPaymentMethod !== 'spl-token') { + if (!isOpen || paymentMethod !== 'spl-token') { setLoadingPrice(false); return; } @@ -60,35 +67,30 @@ export default function PaymentModal({ }; fetchTokenAmount(); - }, [isOpen, selectedPaymentMethod, targetUsdAmount, mintAddress]); + }, [isOpen, paymentMethod, targetUsdAmount, mintAddress]); - // Reset state when modal opens + // Initialize payment method when modal opens useEffect(() => { if (isOpen) { - setSelectedPaymentMethod(null); setError(''); - setTokenAmount(0); setLoadingPrice(false); - } - }, [isOpen]); - const handlePaymentMethodChange = (method: PaymentMethod) => { - setSelectedPaymentMethod(method); - setError(''); - - // Set tokenAmount for SOL payments to maintain consistency - if (method === 'sol') { - setTokenAmount(SOL_PAYMENT_AMOUNT_LAMPORTS); + // Set tokenAmount for SOL payments to maintain consistency + if (paymentMethod === 'sol') { + setTokenAmount(SOL_PAYMENT_AMOUNT_LAMPORTS); + } else { + setTokenAmount(0); + } } - }; + }, [isOpen, paymentMethod]); + const handlePayment = useCallback(async () => { - if (!selectedPaymentMethod) { - setError('Please select a payment method.'); + if (!paymentMethod) { return; } - if (tokenAmount === 0 || (selectedPaymentMethod === 'spl-token' && loadingPrice)) { + if (tokenAmount === 0 || (paymentMethod === 'spl-token' && loadingPrice)) { setError('Payment amount not ready. Please wait.'); return; } @@ -103,20 +105,23 @@ export default function PaymentModal({ try { const paymentRequest: PaymentRequest = { - paymentMethod: selectedPaymentMethod, + paymentMethod: paymentMethod, 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( wallet.adapter, - directConnection, + connectionToUse, publicKey!.toBase58(), paymentRequest ); if (result.success && result.transactionSignature) { - onPaymentComplete(result.transactionSignature, selectedPaymentMethod); + onPaymentComplete(result.transactionSignature, paymentMethod); } else { setError(result.error || 'Payment failed. Please try again.'); } @@ -125,12 +130,10 @@ export default function PaymentModal({ } finally { setLoading(false); } - }, [selectedPaymentMethod, tokenAmount, loadingPrice, wallet, directConnection, publicKey, onPaymentComplete]); + }, [paymentMethod, tokenAmount, loadingPrice, wallet, directConnection, solConnection, publicKey, onPaymentComplete]); const getPaymentAmountDisplay = () => { - if (!selectedPaymentMethod) return ''; - - switch (selectedPaymentMethod) { + switch (paymentMethod) { case 'sol': return `${SOL_PAYMENT_AMOUNT_LAMPORTS / LAMPORTS_PER_SOL} SOL`; case 'spl-token': @@ -141,7 +144,7 @@ export default function PaymentModal({ } }; - if (!isOpen) return null; + if (!isOpen || !paymentMethod) return null; return (
@@ -161,111 +164,93 @@ export default function PaymentModal({
- {/* Payment Method Selection */} + {/* Payment Method Display */}
-
- {Object.entries(PAYMENT_METHOD_LABELS).map(([method, label]) => ( - - ))} +
+

+ {PAYMENT_METHOD_LABELS[paymentMethod]} +

- {/* Payment Details - Only show when method is selected */} - {selectedPaymentMethod && ( - <> -
-

Recipient Address:

-
- {getRecipientAddress(selectedPaymentMethod)} + {/* Payment Details */} +
+

Recipient Address:

+
+ {getRecipientAddress(paymentMethod)} +
+
+ +
+ +
+
+ +
+ USD
-
- -
-
- -
- USD -
+
+ {(paymentMethod === 'spl-token' && loadingPrice) ? ( +
+ + + + + Fetching token amount...
- -
- {(selectedPaymentMethod === 'spl-token' && loadingPrice) ? ( -
- - - - - Fetching token amount... -
- ) : ( - - )} - {!(selectedPaymentMethod === 'spl-token' && loadingPrice) && ( -
- - {selectedPaymentMethod === 'sol' ? 'SOL' : tokenSymbol} - -
- )} + ) : ( + + )} + {!(paymentMethod === 'spl-token' && loadingPrice) && ( +
+ + {paymentMethod === 'sol' ? 'SOL' : tokenSymbol} +
-
- {selectedPaymentMethod === 'spl-token' && ( -

- Token information fetched from Jupiter -

)}
- - )} +
+ {paymentMethod === 'spl-token' && ( +

+ Token information fetched from Jupiter +

+ )} +
{error && (
@@ -291,11 +276,11 @@ export default function PaymentModal({ onClick={handlePayment} className="px-5 py-2 rounded-md flex items-center transition-colors" style={{ - backgroundColor: (loading || loadingPrice || !selectedPaymentMethod) ? 'var(--muted)' : 'var(--primary)', + backgroundColor: (loading || loadingPrice) ? 'var(--muted)' : 'var(--primary)', 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) && ( @@ -303,8 +288,7 @@ export default function PaymentModal({ )} - {!selectedPaymentMethod ? 'Select Payment Method' : - loadingPrice ? 'Loading Price...' : + {loadingPrice ? 'Loading Price...' : loading ? 'Processing...' : 'Pay with Solana Wallet'} diff --git a/src/components/WalletProviders.tsx b/src/components/WalletProviders.tsx index 162d435..165a868 100644 --- a/src/components/WalletProviders.tsx +++ b/src/components/WalletProviders.tsx @@ -10,6 +10,7 @@ import { SolflareWalletAdapter } from '@solana/wallet-adapter-solflare'; // Default styles that can be overridden by your app import '@solana/wallet-adapter-react-ui/styles.css'; import assert from 'assert'; +import { usePaymentMethod } from '@/contexts/PaymentMethodContext'; assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required'); const SOLANA_RPC_URL = process.env.NEXT_PUBLIC_SOLANA_RPC_URL; @@ -19,12 +20,15 @@ interface WalletProvidersProps { } export default function WalletProviders({ children }: WalletProvidersProps) { + const { selectedPaymentMethod } = usePaymentMethod(); + // Configure the Solana network endpoint const endpoint = useMemo(() => { return SOLANA_RPC_URL; }, []); - const wallets = useMemo( + // All available wallet adapters + const allWallets = useMemo( () => [ new PhantomWalletAdapter(), 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 ( diff --git a/src/contexts/PaymentMethodContext.tsx b/src/contexts/PaymentMethodContext.tsx new file mode 100644 index 0000000..91425be --- /dev/null +++ b/src/contexts/PaymentMethodContext.tsx @@ -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(undefined); + +interface PaymentMethodProviderProps { + children: ReactNode; +} + +export function PaymentMethodProvider({ children }: PaymentMethodProviderProps) { + const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(null); + + return ( + + {children} + + ); +} + +export function usePaymentMethod() { + const context = useContext(PaymentMethodContext); + if (context === undefined) { + throw new Error('usePaymentMethod must be used within a PaymentMethodProvider'); + } + return context; +}