diff --git a/src/app/dashboard/rent/Boxes.tsx b/src/app/dashboard/rent/Boxes.tsx index 453a15b..29d27c2 100644 --- a/src/app/dashboard/rent/Boxes.tsx +++ b/src/app/dashboard/rent/Boxes.tsx @@ -156,7 +156,9 @@ export default function Boxes() { ['left', 'right'].map((side) => ( diff --git a/src/app/globals.css b/src/app/globals.css index fe5d95c..9a545ec 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -11,3 +11,26 @@ textarea { .hide-scrollbar::-webkit-scrollbar { display: none; } + +.token-dropdown { + width: 100%; + padding: 10px 12px; + background: #0E1618; + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 3px; + color: #FFFFFF; + font-family: 'Geist Mono', monospace; + font-size: 14px; + line-height: 18px; + letter-spacing: -0.01em; + appearance: none; + -webkit-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%230CE77E' stroke-width='1.5'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; + background-size: 12px; +} +.token-dropdown:focus { + outline: none; + border-color: #0CE77E; +} diff --git a/src/components/GpuPaymentModal.tsx b/src/components/GpuPaymentModal.tsx index 8c12827..0f9686f 100644 --- a/src/components/GpuPaymentModal.tsx +++ b/src/components/GpuPaymentModal.tsx @@ -1,18 +1,23 @@ 'use client'; import { useEffect, useState } from 'react'; +import React from "react"; import { - Connection, PublicKey, Transaction, + sendAndConfirmTransaction, + Connection, SystemProgram, -} from '@solana/web3.js'; +} from "@solana/web3.js"; import axios from 'axios'; import { usePrivy } from '@privy-io/react-auth'; -import { - useSolanaWallets, -} from '@privy-io/react-auth/solana'; +import { useSolanaWallets } from '@privy-io/react-auth/solana'; import { createClient } from '@supabase/supabase-js'; +import { + getAssociatedTokenAddress, + createTransferInstruction, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; interface GpuPaymentModalProps { isOpen: boolean; @@ -21,15 +26,197 @@ interface GpuPaymentModalProps { id: string; title: string; price_usd: number; - price_per_hour: string; // new field + price_per_hour: string; }; } - const SOLANA_RPC = process.env.NEXT_PUBLIC_SOLANA_RPC!; const BUSINESS_WALLET = process.env.NEXT_PUBLIC_BUSINESS_WALLET!; const EMAIL_API_URL = process.env.NEXT_PUBLIC_EMAIL_API_URL!; +const USDC_MINT = new PublicKey("Es9vMFrzaCERJ8gLhEvX5yQceQ2uKcXfUrx2Wcikgqay"); +const JUP_MINT = new PublicKey("JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN"); + +const CornerEdge = ({ position }: { position: 'left-top' | 'right-top' | 'left-bottom' | 'right-bottom' }) => { + const paths = { + 'left-top': ( + <> + + + + ), + 'right-top': ( + <> + + + + ), + 'left-bottom': ( + <> + + + + ), + 'right-bottom': ( + <> + + + + ) + }; + + return ( + + {paths[position]} + + ); +}; + +// Red corner edges for error UI +const RedCornerEdge = ({ position }: { position: 'left-top' | 'right-top' | 'left-bottom' | 'right-bottom' }) => { + const paths = { + 'left-top': ( + <> + + + + ), + 'right-top': ( + <> + + + + ), + 'left-bottom': ( + <> + + + + ), + 'right-bottom': ( + <> + + + + ) + }; + + return ( + + {paths[position]} + + ); +}; + +const SuccessIcon = () => { + return ( +
+
+ +
+ ); +}; + +const ErrorIcon = () => { + return ( +
+
+ + + +
+ ); +}; + +const ErrorBox = () => { + return ( +
+
+
+
+ + Error + + + 404 (Payment Error) + +
+ + + + +
+ ); +}; + +interface OrderIdBoxProps { + orderId: string; +} + +const OrderIdBox = ({ orderId }: OrderIdBoxProps) => { + return ( +
+
+
+
+ + Order ID + + + {orderId} + +
+ + + + +
+ ); +}; + export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) => { const { user, connectWallet } = usePrivy(); const { wallets: privyWallets } = useSolanaWallets(); @@ -39,12 +226,15 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) const [loading, setLoading] = useState(false); const [errorMsg, setErrorMsg] = useState(null); const [successMsg, setSuccessMsg] = useState(null); + const [paymentSuccess, setPaymentSuccess] = useState(false); + const [paymentFailure, setPaymentFailure] = useState(false); + const [orderId, setOrderId] = useState(''); const connection = new Connection(SOLANA_RPC); const userEmail = user?.email?.address || emailInput || null; - const isValidEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); const [emailTouched, setEmailTouched] = useState(false); + const [selectedToken, setSelectedToken] = useState<'SOL' | 'USDC' | 'JUP'>('SOL'); const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, @@ -67,6 +257,8 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) fetchSolPrice(); setErrorMsg(null); setSuccessMsg(null); + setPaymentSuccess(false); + setPaymentFailure(false); } }, [isOpen]); @@ -77,158 +269,363 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) try { if (!userEmail || !isValidEmail(userEmail)) { - throw new Error('Please enter a valid email address for your receipt.'); + throw new Error("Please enter a valid email address for your receipt."); } const solWallet = privyWallets[0]; - - if (!solWallet || !solWallet.address) { - throw new Error('No connected Solana wallet found. Please connect a wallet via Privy.'); - } - - if (!solWallet.signTransaction) { - throw new Error('Connected wallet does not support transaction signing.'); + if (!solWallet?.address || !solWallet.signTransaction) { + throw new Error("No connected Solana wallet found or wallet can't sign."); } const fromPubKey = new PublicKey(solWallet.address); const toPubKey = new PublicKey(BUSINESS_WALLET); - - const solAmount = (gpu.price_usd / (solPrice || 1)).toFixed(6); + const transaction = new Transaction(); + let txId = ""; + let tokenUsed = selectedToken; + let amountDisplay = ""; - const transaction = new Transaction().add( - SystemProgram.transfer({ - fromPubkey: fromPubKey, - toPubkey: toPubKey, - lamports: Math.floor(parseFloat(solAmount) * 1e9), - }) - ); + if (selectedToken === "SOL") { + const { data } = await axios.get( + "https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd" + ); + const solPrice = data.solana.usd; + const solAmount = (gpu.price_usd / solPrice).toFixed(6); + amountDisplay = `${solAmount} SOL`; + + transaction.add( + SystemProgram.transfer({ + fromPubkey: fromPubKey, + toPubkey: toPubKey, + lamports: Math.floor(parseFloat(solAmount) * 1e9), + }) + ); + } else { + const selectedMint = selectedToken === "USDC" ? USDC_MINT : JUP_MINT; + const tokenDecimals = 6; + + let tokenPrice = 1; + if (selectedToken === "JUP") { + const { data } = await axios.get( + "https://api.coingecko.com/api/v3/simple/price?ids=jupiter-exchange&vs_currencies=usd" + ); + tokenPrice = data["jupiter-exchange"]?.usd ?? 0.5; + } + + const tokenAmount = (gpu.price_usd / tokenPrice).toFixed(tokenDecimals); + amountDisplay = `${tokenAmount} ${selectedToken}`; + + const fromTokenAccount = await getAssociatedTokenAddress(selectedMint, fromPubKey); + const toTokenAccount = await getAssociatedTokenAddress(selectedMint, toPubKey); + + transaction.add( + createTransferInstruction( + fromTokenAccount, + toTokenAccount, + fromPubKey, + BigInt(Math.floor(parseFloat(tokenAmount) * Math.pow(10, tokenDecimals))), + [], + TOKEN_PROGRAM_ID + ) + ); + } transaction.feePayer = fromPubKey; const { blockhash } = await connection.getLatestBlockhash(); transaction.recentBlockhash = blockhash; const signedTx = await solWallet.signTransaction(transaction); + txId = await connection.sendRawTransaction(signedTx.serialize()); + await connection.confirmTransaction(txId, "confirmed"); - const txId = await connection.sendRawTransaction(signedTx.serialize()); - await connection.confirmTransaction(txId, 'confirmed'); - - // Send confirmation email await axios.post(EMAIL_API_URL, { email: userEmail, product: gpu.title, price_hour: gpu.price_per_hour, price: gpu.price_usd.toFixed(2), - }); + token: selectedToken, + }); - // Store order in Supabase - const { error } = await supabase.from('orders').insert([ + // Generate a random order ID (in production this would come from the backend) + const generatedOrderId = `${Math.floor(Math.random() * 900) + 100}-${Math.floor(Math.random() * 900) + 100}-${Math.floor(Math.random() * 900) + 100}`; + setOrderId(generatedOrderId); + + const { error } = await supabase.from("orders").insert([ { - gpu_id: gpu.id, + gpu: `${gpu.id} ${gpu.title}`, user_email: userEmail, - amount_sol: parseFloat(solAmount), - sol_tx_signature: txId, - status: 'success', + amount_usd: gpu.price_usd, + token_used: tokenUsed, + token_amount: amountDisplay, + token_tx_signature: txId, + status: "success", + order_id: generatedOrderId, }, ]); - if (error) { - throw new Error(`Failed to save order: ${error.message}`); - } - - + if (error) throw new Error(`Failed to save order: ${error.message}`); setSuccessMsg(`Payment successful! Transaction ID: ${txId}`); + setPaymentSuccess(true); } catch (err: any) { - setErrorMsg(err.message || 'Payment failed.'); + console.error("Payment error:", err); + setErrorMsg(err.message || "Payment failed."); + setPaymentFailure(true); + + // Log the failed attempt in Supabase + const failedOrderId = `FAIL-${Math.floor(Math.random() * 900) + 100}-${Math.floor(Math.random() * 900) + 100}`; + await supabase.from("orders").insert([ + { + gpu: `${gpu.id} ${gpu.title}`, + user_email: userEmail, + amount_usd: gpu.price_usd, + token_used: selectedToken, + status: "failed", + error_message: err.message || "Unknown error", + order_id: failedOrderId, + }, + ]); } finally { setLoading(false); } }; + const handleRetry = () => { + // Reset failure state and error message + setPaymentFailure(false); + setErrorMsg(null); + // Don't reset other form values so user doesn't have to start over + }; + if (!isOpen) return null; const solAmount = solPrice ? (gpu.price_usd / solPrice).toFixed(6) : '...'; - return ( -
-
-

{gpu.title}

-

- Price: ${gpu.price_usd} USD (~{solAmount} SOL) -

+ // Show failure screen if payment failed + if (paymentFailure) { + return ( +
+
+ + + + + + + +
+

+ PURCHASE FAILED +

+

+ Your purchase didn't go through +

+
+ + + +
+ + + +
+
+
+ ); + } + // Show success screen if payment was successful + if (paymentSuccess) { + return ( +
+
+ + + + + + + +
+

+ PURCHASE SUCCESSFUL +

+

+ Thank you for shopping with Vertex +

+
+ + + + +
+
+ ); + } + + // Show payment form + return ( +
+
+ {/* Corner decoration elements */} + + + + + + {/* Header */} +
+

+ Selected configuration +

+

+ {gpu.title} +

+
+ + {/* Price Display */} +
+ + + + + +
+ Total Price +
+ ${gpu.price_usd} + (~{solAmount} SOL) +
+
+
+ + {/* Email Input */} + {!user?.email?.address && ( +
+ YOUR EMAIL ADDRESS +
+ + + + + + setEmailInput(e.target.value)} + onBlur={() => setEmailTouched(true)} + className="w-full p-3 bg-transparent text-[#0CE77E] placeholder-[#0CE77E]/50 focus:outline-none text-base" + /> +
+ {emailTouched && emailInput && !isValidEmail(emailInput) && ( +

Please enter a valid email address.

+ )} +
+ )} + + {/* Token Selection */} +
+ SELECT PAYMENT TOKEN +
+ + + + + + +
+
+ + {/* Error and Success Messages */} {errorMsg && ( -
+
{errorMsg}
)} - + {successMsg && ( -
+
{successMsg}
)} - - {/* Wallet not connected */} - {privyWallets.length === 0 ? ( -
-

- To proceed with payment, please connect a Solana wallet. -

- -
- ) : ( - <> - {/* Show email prompt if not logged in with email */} - {!user?.email?.address && ( -
- - setEmailInput(e.target.value)} - onBlur={() => setEmailTouched(true)} - className="w-full px-3 py-2 border rounded-md text-sm" - /> - {emailTouched && emailInput && !isValidEmail(emailInput) && ( -

Please enter a valid email address.

- )} - -
- )} - -
- - -
- - )} + + {/* Action Buttons */} +
+ {/* Confirm Button */} + + + {/* Cancel Button */} + +
); }; -export default GpuPaymentModal; +export default GpuPaymentModal; \ No newline at end of file