added new ui
This commit is contained in:
parent
df220a533f
commit
42350c9acc
@ -156,7 +156,9 @@ export default function Boxes() {
|
|||||||
['left', 'right'].map((side) => (
|
['left', 'right'].map((side) => (
|
||||||
<span
|
<span
|
||||||
key={`${pos}-${side}`}
|
key={`${pos}-${side}`}
|
||||||
className={`absolute -${pos}-px -${side}-px opacity-25 group-hover:opacity-100 duration-200`}
|
className={`absolute -${pos}-px -${side}-px opacity-25 group-hover:opacity-100 duration-200 ${
|
||||||
|
side === 'right' ? 'scale-x-[-1]' : ''
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<svg width="6" height="5" viewBox="0 0 6 5" fill="none">
|
<svg width="6" height="5" viewBox="0 0 6 5" fill="none">
|
||||||
<path d={pos === 'top' ? 'M1 0V4.5' : 'M1 4.5V0'} stroke="#0CE77E" />
|
<path d={pos === 'top' ? 'M1 0V4.5' : 'M1 4.5V0'} stroke="#0CE77E" />
|
||||||
|
@ -11,3 +11,26 @@ textarea {
|
|||||||
.hide-scrollbar::-webkit-scrollbar {
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
display: none;
|
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;
|
||||||
|
}
|
||||||
|
@ -1,18 +1,23 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
Connection,
|
|
||||||
PublicKey,
|
PublicKey,
|
||||||
Transaction,
|
Transaction,
|
||||||
|
sendAndConfirmTransaction,
|
||||||
|
Connection,
|
||||||
SystemProgram,
|
SystemProgram,
|
||||||
} from '@solana/web3.js';
|
} from "@solana/web3.js";
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { usePrivy } from '@privy-io/react-auth';
|
import { usePrivy } from '@privy-io/react-auth';
|
||||||
import {
|
import { useSolanaWallets } from '@privy-io/react-auth/solana';
|
||||||
useSolanaWallets,
|
|
||||||
} from '@privy-io/react-auth/solana';
|
|
||||||
import { createClient } from '@supabase/supabase-js';
|
import { createClient } from '@supabase/supabase-js';
|
||||||
|
import {
|
||||||
|
getAssociatedTokenAddress,
|
||||||
|
createTransferInstruction,
|
||||||
|
TOKEN_PROGRAM_ID,
|
||||||
|
} from "@solana/spl-token";
|
||||||
|
|
||||||
interface GpuPaymentModalProps {
|
interface GpuPaymentModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -21,15 +26,197 @@ interface GpuPaymentModalProps {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
price_usd: number;
|
price_usd: number;
|
||||||
price_per_hour: string; // new field
|
price_per_hour: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const SOLANA_RPC = process.env.NEXT_PUBLIC_SOLANA_RPC!;
|
const SOLANA_RPC = process.env.NEXT_PUBLIC_SOLANA_RPC!;
|
||||||
const BUSINESS_WALLET = process.env.NEXT_PUBLIC_BUSINESS_WALLET!;
|
const BUSINESS_WALLET = process.env.NEXT_PUBLIC_BUSINESS_WALLET!;
|
||||||
const EMAIL_API_URL = process.env.NEXT_PUBLIC_EMAIL_API_URL!;
|
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': (
|
||||||
|
<>
|
||||||
|
<path d="M1 0V4.5" stroke="#0CE77E" strokeWidth="1" />
|
||||||
|
<path d="M5.5 0.5L1 0.5" stroke="#0CE77E" strokeWidth="1" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
'right-top': (
|
||||||
|
<>
|
||||||
|
<path d="M5 0V4.5" stroke="#0CE77E" strokeWidth="1" />
|
||||||
|
<path d="M0.5 0.5L5 0.5" stroke="#0CE77E" strokeWidth="1" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
'left-bottom': (
|
||||||
|
<>
|
||||||
|
<path d="M1 4.5V0" stroke="#0CE77E" strokeWidth="1" />
|
||||||
|
<path d="M5.5 4L1 4" stroke="#0CE77E" strokeWidth="1" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
'right-bottom': (
|
||||||
|
<>
|
||||||
|
<path d="M5 4.5V0" stroke="#0CE77E" strokeWidth="1" />
|
||||||
|
<path d="M0.5 4L5 4" stroke="#0CE77E" strokeWidth="1" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="6"
|
||||||
|
height="5"
|
||||||
|
viewBox="0 0 6 5"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={`absolute ${position.includes('left') ? 'left-0' : 'right-0'} ${position.includes('top') ? 'top-0' : 'bottom-0'}`}
|
||||||
|
>
|
||||||
|
{paths[position]}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Red corner edges for error UI
|
||||||
|
const RedCornerEdge = ({ position }: { position: 'left-top' | 'right-top' | 'left-bottom' | 'right-bottom' }) => {
|
||||||
|
const paths = {
|
||||||
|
'left-top': (
|
||||||
|
<>
|
||||||
|
<path d="M1 0V4.5" stroke="#F62727" strokeWidth="1" />
|
||||||
|
<path d="M5.5 0.5L1 0.5" stroke="#F62727" strokeWidth="1" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
'right-top': (
|
||||||
|
<>
|
||||||
|
<path d="M5 0V4.5" stroke="#F62727" strokeWidth="1" />
|
||||||
|
<path d="M0.5 0.5L5 0.5" stroke="#F62727" strokeWidth="1" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
'left-bottom': (
|
||||||
|
<>
|
||||||
|
<path d="M1 4.5V0" stroke="#F62727" strokeWidth="1" />
|
||||||
|
<path d="M5.5 4L1 4" stroke="#F62727" strokeWidth="1" />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
'right-bottom': (
|
||||||
|
<>
|
||||||
|
<path d="M5 4.5V0" stroke="#F62727" strokeWidth="1" />
|
||||||
|
<path d="M0.5 4L5 4" stroke="#F62727" strokeWidth="1" />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="6"
|
||||||
|
height="5"
|
||||||
|
viewBox="0 0 6 5"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className={`absolute ${position.includes('left') ? 'left-0' : 'right-0'} ${position.includes('top') ? 'top-0' : 'bottom-0'}`}
|
||||||
|
>
|
||||||
|
{paths[position]}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SuccessIcon = () => {
|
||||||
|
return (
|
||||||
|
<div className="relative h-[54px] w-[54px]">
|
||||||
|
<div className="absolute top-0 left-0 bg-emerald-500 rounded-xl h-[54px] w-[54px]" />
|
||||||
|
<svg
|
||||||
|
width="39"
|
||||||
|
height="38"
|
||||||
|
viewBox="0 0 39 38"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="absolute top-2 left-2"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M32.1663 9.5L14.7497 26.9167L6.83301 19"
|
||||||
|
stroke="black"
|
||||||
|
strokeWidth="3.16667"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ErrorIcon = () => {
|
||||||
|
return (
|
||||||
|
<div className="relative h-[54px] w-[54px]">
|
||||||
|
<div className="absolute bg-red-600 rounded-xl h-[54px] w-[54px]" />
|
||||||
|
<svg
|
||||||
|
width="39"
|
||||||
|
height="38"
|
||||||
|
viewBox="0 0 39 38"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="absolute w-[38px] h-[38px] left-[9px] top-[8px]"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M29 9.5L10 28.5M10 9.5L29 28.5"
|
||||||
|
stroke="black"
|
||||||
|
strokeWidth="3.16667"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ErrorBox = () => {
|
||||||
|
return (
|
||||||
|
<div className="relative w-full max-w-[357px] h-[60px]">
|
||||||
|
<div className="absolute inset-0 bg-[#F62727] bg-opacity-10" />
|
||||||
|
<div className="absolute inset-[1px] border border-white border-opacity-5" />
|
||||||
|
<div className="absolute flex justify-between items-center px-5 w-full h-full">
|
||||||
|
<span className="font-mono text-[18px] font-medium text-white">
|
||||||
|
Error
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-[18px] font-medium text-[#F62727]">
|
||||||
|
404 (Payment Error)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<RedCornerEdge position="left-top" />
|
||||||
|
<RedCornerEdge position="right-top" />
|
||||||
|
<RedCornerEdge position="left-bottom" />
|
||||||
|
<RedCornerEdge position="right-bottom" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface OrderIdBoxProps {
|
||||||
|
orderId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OrderIdBox = ({ orderId }: OrderIdBoxProps) => {
|
||||||
|
return (
|
||||||
|
<div className="relative w-full max-w-[357px] h-[60px]">
|
||||||
|
<div className="absolute inset-0 bg-[#0CE77E] bg-opacity-10" />
|
||||||
|
<div className="absolute inset-[1px] border border-white border-opacity-5" />
|
||||||
|
<div className="absolute flex justify-between items-center px-5 w-full h-full">
|
||||||
|
<span className="font-mono text-[18px] font-medium text-white">
|
||||||
|
Order ID
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-[18px] font-medium text-[#0CE77E]">
|
||||||
|
{orderId}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<CornerEdge position="left-top" />
|
||||||
|
<CornerEdge position="right-top" />
|
||||||
|
<CornerEdge position="left-bottom" />
|
||||||
|
<CornerEdge position="right-bottom" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) => {
|
export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) => {
|
||||||
const { user, connectWallet } = usePrivy();
|
const { user, connectWallet } = usePrivy();
|
||||||
const { wallets: privyWallets } = useSolanaWallets();
|
const { wallets: privyWallets } = useSolanaWallets();
|
||||||
@ -39,12 +226,15 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps)
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||||
const [successMsg, setSuccessMsg] = useState<string | null>(null);
|
const [successMsg, setSuccessMsg] = useState<string | null>(null);
|
||||||
|
const [paymentSuccess, setPaymentSuccess] = useState(false);
|
||||||
|
const [paymentFailure, setPaymentFailure] = useState(false);
|
||||||
|
const [orderId, setOrderId] = useState<string>('');
|
||||||
|
|
||||||
const connection = new Connection(SOLANA_RPC);
|
const connection = new Connection(SOLANA_RPC);
|
||||||
const userEmail = user?.email?.address || emailInput || null;
|
const userEmail = user?.email?.address || emailInput || null;
|
||||||
|
|
||||||
const isValidEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
const isValidEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||||
const [emailTouched, setEmailTouched] = useState(false);
|
const [emailTouched, setEmailTouched] = useState(false);
|
||||||
|
const [selectedToken, setSelectedToken] = useState<'SOL' | 'USDC' | 'JUP'>('SOL');
|
||||||
|
|
||||||
const supabase = createClient(
|
const supabase = createClient(
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
@ -67,6 +257,8 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps)
|
|||||||
fetchSolPrice();
|
fetchSolPrice();
|
||||||
setErrorMsg(null);
|
setErrorMsg(null);
|
||||||
setSuccessMsg(null);
|
setSuccessMsg(null);
|
||||||
|
setPaymentSuccess(false);
|
||||||
|
setPaymentFailure(false);
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
@ -77,158 +269,363 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps)
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (!userEmail || !isValidEmail(userEmail)) {
|
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];
|
const solWallet = privyWallets[0];
|
||||||
|
if (!solWallet?.address || !solWallet.signTransaction) {
|
||||||
if (!solWallet || !solWallet.address) {
|
throw new Error("No connected Solana wallet found or wallet can't sign.");
|
||||||
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.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fromPubKey = new PublicKey(solWallet.address);
|
const fromPubKey = new PublicKey(solWallet.address);
|
||||||
const toPubKey = new PublicKey(BUSINESS_WALLET);
|
const toPubKey = new PublicKey(BUSINESS_WALLET);
|
||||||
|
const transaction = new Transaction();
|
||||||
const solAmount = (gpu.price_usd / (solPrice || 1)).toFixed(6);
|
let txId = "";
|
||||||
|
let tokenUsed = selectedToken;
|
||||||
|
let amountDisplay = "";
|
||||||
|
|
||||||
const transaction = new Transaction().add(
|
if (selectedToken === "SOL") {
|
||||||
SystemProgram.transfer({
|
const { data } = await axios.get(
|
||||||
fromPubkey: fromPubKey,
|
"https://api.coingecko.com/api/v3/simple/price?ids=solana&vs_currencies=usd"
|
||||||
toPubkey: toPubKey,
|
);
|
||||||
lamports: Math.floor(parseFloat(solAmount) * 1e9),
|
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;
|
transaction.feePayer = fromPubKey;
|
||||||
const { blockhash } = await connection.getLatestBlockhash();
|
const { blockhash } = await connection.getLatestBlockhash();
|
||||||
transaction.recentBlockhash = blockhash;
|
transaction.recentBlockhash = blockhash;
|
||||||
|
|
||||||
const signedTx = await solWallet.signTransaction(transaction);
|
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, {
|
await axios.post(EMAIL_API_URL, {
|
||||||
email: userEmail,
|
email: userEmail,
|
||||||
product: gpu.title,
|
product: gpu.title,
|
||||||
price_hour: gpu.price_per_hour,
|
price_hour: gpu.price_per_hour,
|
||||||
price: gpu.price_usd.toFixed(2),
|
price: gpu.price_usd.toFixed(2),
|
||||||
});
|
token: selectedToken,
|
||||||
|
});
|
||||||
|
|
||||||
// Store order in Supabase
|
// Generate a random order ID (in production this would come from the backend)
|
||||||
const { error } = await supabase.from('orders').insert([
|
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,
|
user_email: userEmail,
|
||||||
amount_sol: parseFloat(solAmount),
|
amount_usd: gpu.price_usd,
|
||||||
sol_tx_signature: txId,
|
token_used: tokenUsed,
|
||||||
status: 'success',
|
token_amount: amountDisplay,
|
||||||
|
token_tx_signature: txId,
|
||||||
|
status: "success",
|
||||||
|
order_id: generatedOrderId,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (error) {
|
if (error) throw new Error(`Failed to save order: ${error.message}`);
|
||||||
throw new Error(`Failed to save order: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
setSuccessMsg(`Payment successful! Transaction ID: ${txId}`);
|
setSuccessMsg(`Payment successful! Transaction ID: ${txId}`);
|
||||||
|
setPaymentSuccess(true);
|
||||||
} catch (err: any) {
|
} 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 {
|
} finally {
|
||||||
setLoading(false);
|
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;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
const solAmount = solPrice ? (gpu.price_usd / solPrice).toFixed(6) : '...';
|
const solAmount = solPrice ? (gpu.price_usd / solPrice).toFixed(6) : '...';
|
||||||
|
|
||||||
return (
|
// Show failure screen if payment failed
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
if (paymentFailure) {
|
||||||
<div className="bg-white w-full max-w-md rounded-xl p-6 shadow-xl">
|
return (
|
||||||
<h2 className="text-xl font-semibold text-gray-800">{gpu.title}</h2>
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80">
|
||||||
<p className="text-gray-600 mt-1">
|
<div className="relative flex flex-col gap-8 items-center p-6 bg-[#0E1618] border border-white border-opacity-10 w-full max-w-lg mx-4">
|
||||||
Price: <span className="font-medium">${gpu.price_usd} USD</span> (~{solAmount} SOL)
|
<RedCornerEdge position="left-top" />
|
||||||
</p>
|
<RedCornerEdge position="right-top" />
|
||||||
|
<RedCornerEdge position="left-bottom" />
|
||||||
|
<RedCornerEdge position="right-bottom" />
|
||||||
|
|
||||||
|
<ErrorIcon />
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1.5 items-center">
|
||||||
|
<h1 className="text-2xl text-center text-white">
|
||||||
|
PURCHASE FAILED
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg text-center text-white opacity-50">
|
||||||
|
Your purchase didn't go through
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ErrorBox />
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button
|
||||||
|
onClick={handleRetry}
|
||||||
|
className="relative w-[157px] h-[51px]"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-[#F62727] bg-opacity-25" />
|
||||||
|
<div className="absolute inset-[1px] border border-white border-opacity-5" />
|
||||||
|
<span className="absolute inset-0 flex items-center justify-center font-mono text-[17.85px] font-medium text-[#F62727]">
|
||||||
|
RETRY
|
||||||
|
</span>
|
||||||
|
<RedCornerEdge position="left-top" />
|
||||||
|
<RedCornerEdge position="right-top" />
|
||||||
|
<RedCornerEdge position="left-bottom" />
|
||||||
|
<RedCornerEdge position="right-bottom" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="relative w-[157px] h-[51px]"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-[#333333]" />
|
||||||
|
<div className="absolute inset-[1px] border border-white border-opacity-5" />
|
||||||
|
<span className="absolute inset-0 flex items-center justify-center font-mono text-[17.85px] font-medium text-white">
|
||||||
|
CLOSE
|
||||||
|
</span>
|
||||||
|
<CornerEdge position="left-top" />
|
||||||
|
<CornerEdge position="right-top" />
|
||||||
|
<CornerEdge position="left-bottom" />
|
||||||
|
<CornerEdge position="right-bottom" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show success screen if payment was successful
|
||||||
|
if (paymentSuccess) {
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80">
|
||||||
|
<div className="relative flex flex-col gap-8 items-center p-6 bg-[#0E1618] border border-white border-opacity-10 w-full max-w-lg mx-4">
|
||||||
|
<CornerEdge position="left-top" />
|
||||||
|
<CornerEdge position="right-top" />
|
||||||
|
<CornerEdge position="left-bottom" />
|
||||||
|
<CornerEdge position="right-bottom" />
|
||||||
|
|
||||||
|
<SuccessIcon />
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1.5 items-center">
|
||||||
|
<h1 className="text-2xl text-center text-white">
|
||||||
|
PURCHASE SUCCESSFUL
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg text-center text-white opacity-50">
|
||||||
|
Thank you for shopping with Vertex
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<OrderIdBox orderId={orderId} />
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="relative w-[157px] h-[51px]"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-[#0CE77E] bg-opacity-25" />
|
||||||
|
<div className="absolute inset-[1px] border border-white border-opacity-5" />
|
||||||
|
<span className="absolute inset-0 flex items-center justify-center font-mono text-[17.85px] font-medium text-[#0CE77E]">
|
||||||
|
CLOSE
|
||||||
|
</span>
|
||||||
|
<CornerEdge position="left-top" />
|
||||||
|
<CornerEdge position="right-top" />
|
||||||
|
<CornerEdge position="left-bottom" />
|
||||||
|
<CornerEdge position="right-bottom" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show payment form
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80">
|
||||||
|
<div className="relative flex flex-col gap-5 p-6 bg-[#0E1618] border border-white border-opacity-10 w-full max-w-lg mx-4">
|
||||||
|
{/* Corner decoration elements */}
|
||||||
|
<CornerEdge position="left-top" />
|
||||||
|
<CornerEdge position="right-top" />
|
||||||
|
<CornerEdge position="left-bottom" />
|
||||||
|
<CornerEdge position="right-bottom" />
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<h2 className="text-sm font-medium text-white text-opacity-50">
|
||||||
|
Selected configuration
|
||||||
|
</h2>
|
||||||
|
<h1 className="text-xl font-medium text-[#0CE77E]">
|
||||||
|
{gpu.title}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Price Display */}
|
||||||
|
<div className="relative border border-white border-opacity-10 bg-[#0CE77E]/10">
|
||||||
|
<CornerEdge position="left-top" />
|
||||||
|
<CornerEdge position="right-top" />
|
||||||
|
<CornerEdge position="left-bottom" />
|
||||||
|
<CornerEdge position="right-bottom" />
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center p-3 w-full">
|
||||||
|
<span className="text-base font-medium text-white">Total Price</span>
|
||||||
|
<div className="flex gap-3 items-center">
|
||||||
|
<span className="text-base font-bold text-[#0CE77E]">${gpu.price_usd}</span>
|
||||||
|
<span className="text-base text-[#0CE77E]">(~{solAmount} SOL)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Email Input */}
|
||||||
|
{!user?.email?.address && (
|
||||||
|
<div className="relative">
|
||||||
|
<span className="block mb-1 text-sm font-medium text-white">YOUR EMAIL ADDRESS</span>
|
||||||
|
<div className="relative border border-white border-opacity-10 bg-[#0CE77E]/10">
|
||||||
|
<CornerEdge position="left-top" />
|
||||||
|
<CornerEdge position="right-top" />
|
||||||
|
<CornerEdge position="left-bottom" />
|
||||||
|
<CornerEdge position="right-bottom" />
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="ENTER YOUR EMAIL"
|
||||||
|
value={emailInput}
|
||||||
|
onChange={(e) => setEmailInput(e.target.value)}
|
||||||
|
onBlur={() => setEmailTouched(true)}
|
||||||
|
className="w-full p-3 bg-transparent text-[#0CE77E] placeholder-[#0CE77E]/50 focus:outline-none text-base"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{emailTouched && emailInput && !isValidEmail(emailInput) && (
|
||||||
|
<p className="mt-1 text-xs text-red-400">Please enter a valid email address.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Token Selection */}
|
||||||
|
<div className="relative">
|
||||||
|
<span className="block mb-1 text-sm font-medium text-white">SELECT PAYMENT TOKEN</span>
|
||||||
|
<div className="relative border border-white border-opacity-10 bg-[#0CE77E]/10">
|
||||||
|
<CornerEdge position="left-top" />
|
||||||
|
<CornerEdge position="right-top" />
|
||||||
|
<CornerEdge position="left-bottom" />
|
||||||
|
<CornerEdge position="right-bottom" />
|
||||||
|
|
||||||
|
<select
|
||||||
|
value={selectedToken}
|
||||||
|
onChange={(e) => setSelectedToken(e.target.value as 'SOL' | 'USDC' | 'JUP')}
|
||||||
|
className="w-full p-3 bg-transparent text-[#0CE77E] focus:outline-none text-base cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value="SOL" className="bg-[#0E1618] text-[#0CE77E]">SOL</option>
|
||||||
|
<option value="USDC" className="bg-[#0E1618] text-[#0CE77E]">USDC</option>
|
||||||
|
<option value="JUP" className="bg-[#0E1618] text-[#0CE77E]">JUP</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error and Success Messages */}
|
||||||
{errorMsg && (
|
{errorMsg && (
|
||||||
<div className="mt-4 rounded-md bg-red-100 px-4 py-2 text-sm text-red-700">
|
<div className="p-2 text-sm text-red-400 border border-red-400 bg-red-400/10">
|
||||||
{errorMsg}
|
{errorMsg}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{successMsg && (
|
{successMsg && (
|
||||||
<div className="mt-4 rounded-md bg-green-100 px-4 py-2 text-sm text-green-700">
|
<div className="p-2 text-sm text-[#0CE77E] border border-[#0CE77E] bg-[#0CE77E]/10">
|
||||||
{successMsg}
|
{successMsg}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Wallet not connected */}
|
{/* Action Buttons */}
|
||||||
{privyWallets.length === 0 ? (
|
<div className="flex justify-between gap-3 mt-2">
|
||||||
<div className="mt-6 text-center">
|
{/* Confirm Button */}
|
||||||
<p className="text-sm text-gray-700 mb-4">
|
<button
|
||||||
To proceed with payment, please connect a Solana wallet.
|
onClick={handlePayment}
|
||||||
</p>
|
disabled={loading || (!user?.email?.address && (!emailInput || !isValidEmail(emailInput)))}
|
||||||
<button
|
className="relative flex-1 py-3 bg-[#004d33] border border-[#0CE77E]/30 text-[#0CE77E] text-sm font-medium disabled:opacity-50 transition-all hover:bg-[#006644] hover:border-[#0CE77E]/50 hover:shadow-[0_0_12px_rgba(12,231,126,0.3)]"
|
||||||
onClick={connectWallet}
|
>
|
||||||
className="px-4 py-2 rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 transition"
|
<CornerEdge position="left-top" />
|
||||||
>
|
<CornerEdge position="right-top" />
|
||||||
Connect Wallet
|
<CornerEdge position="left-bottom" />
|
||||||
</button>
|
<CornerEdge position="right-bottom" />
|
||||||
</div>
|
{loading ? 'PROCESSING...' : 'CONFIRM PAYMENT'}
|
||||||
) : (
|
</button>
|
||||||
<>
|
|
||||||
{/* Show email prompt if not logged in with email */}
|
{/* Cancel Button */}
|
||||||
{!user?.email?.address && (
|
<button
|
||||||
<div className="mt-4">
|
onClick={onClose}
|
||||||
<label className="block text-sm text-gray-700 mb-1">
|
disabled={loading}
|
||||||
Email for invoice & access:
|
className="relative flex-1 py-3 bg-[#333333] border border-white/30 text-white text-sm font-medium disabled:opacity-50 transition-all hover:bg-[#444444] hover:border-white/50 hover:shadow-[0_0_12px_rgba(255,255,255,0.2)]"
|
||||||
</label>
|
>
|
||||||
<input
|
<CornerEdge position="left-top" />
|
||||||
type="email"
|
<CornerEdge position="right-top" />
|
||||||
placeholder="you@example.com"
|
<CornerEdge position="left-bottom" />
|
||||||
value={emailInput}
|
<CornerEdge position="right-bottom" />
|
||||||
onChange={(e) => setEmailInput(e.target.value)}
|
CANCEL
|
||||||
onBlur={() => setEmailTouched(true)}
|
</button>
|
||||||
className="w-full px-3 py-2 border rounded-md text-sm"
|
</div>
|
||||||
/>
|
|
||||||
{emailTouched && emailInput && !isValidEmail(emailInput) && (
|
|
||||||
<p className="mt-1 text-xs text-red-600">Please enter a valid email address.</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-6 flex justify-end gap-3">
|
|
||||||
<button
|
|
||||||
onClick={onClose}
|
|
||||||
disabled={loading}
|
|
||||||
className="px-4 py-2 rounded-lg border border-gray-300 text-gray-600 hover:bg-gray-100 transition"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={handlePayment}
|
|
||||||
disabled={
|
|
||||||
loading ||
|
|
||||||
(!user?.email?.address && (!emailInput || !isValidEmail(emailInput)))
|
|
||||||
}
|
|
||||||
|
|
||||||
className="px-4 py-2 rounded-lg bg-green-600 text-white hover:bg-green-700 transition disabled:opacity-60"
|
|
||||||
>
|
|
||||||
{loading ? 'Processing...' : 'Confirm Payment'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GpuPaymentModal;
|
export default GpuPaymentModal;
|
Loading…
x
Reference in New Issue
Block a user