payment system fix
This commit is contained in:
parent
a2af10606e
commit
6524ea415e
@ -171,14 +171,15 @@ export default function Boxes() {
|
|||||||
|
|
||||||
{selectedGpu && (
|
{selectedGpu && (
|
||||||
<GpuPaymentModal
|
<GpuPaymentModal
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
onClose={() => setSelectedGpu(null)}
|
onClose={() => setSelectedGpu(null)}
|
||||||
gpu={{
|
gpu={{
|
||||||
id: selectedGpu.id,
|
id: selectedGpu.id,
|
||||||
title: selectedGpu.title,
|
title: selectedGpu.title,
|
||||||
price_usd: selectedGpu.price_usd,
|
price_usd: selectedGpu.price_usd,
|
||||||
}}
|
price_per_hour: selectedGpu.price, // pass it here
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -11,8 +11,8 @@ import axios from 'axios';
|
|||||||
import { usePrivy } from '@privy-io/react-auth';
|
import { usePrivy } from '@privy-io/react-auth';
|
||||||
import {
|
import {
|
||||||
useSolanaWallets,
|
useSolanaWallets,
|
||||||
useSignTransaction,
|
|
||||||
} from '@privy-io/react-auth/solana';
|
} from '@privy-io/react-auth/solana';
|
||||||
|
import { createClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
interface GpuPaymentModalProps {
|
interface GpuPaymentModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -21,23 +21,35 @@ interface GpuPaymentModalProps {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
price_usd: number;
|
price_usd: number;
|
||||||
|
price_per_hour: string; // new field
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const SOLANA_RPC = 'https://api.mainnet-beta.solana.com';
|
|
||||||
const BUSINESS_WALLET = 'Y93ednSpre2XRjPTBafHU1BXPXKMPhujcYAEshS5pXm8K'; // <-- Replace this
|
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!;
|
||||||
|
|
||||||
export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) => {
|
export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps) => {
|
||||||
const { user } = usePrivy();
|
const { user, connectWallet } = usePrivy();
|
||||||
const { wallets } = useSolanaWallets();
|
const { wallets: privyWallets } = useSolanaWallets();
|
||||||
const { signTransaction } = useSignTransaction();
|
|
||||||
|
|
||||||
const [solPrice, setSolPrice] = useState<number | null>(null);
|
const [solPrice, setSolPrice] = useState<number | null>(null);
|
||||||
|
const [emailInput, setEmailInput] = useState('');
|
||||||
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 connection = new Connection(SOLANA_RPC);
|
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 supabase = createClient(
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchSolPrice = async () => {
|
const fetchSolPrice = async () => {
|
||||||
@ -64,13 +76,23 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps)
|
|||||||
setSuccessMsg(null);
|
setSuccessMsg(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const solWallet = wallets[0];
|
if (!userEmail || !isValidEmail(userEmail)) {
|
||||||
|
throw new Error('Please enter a valid email address for your receipt.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const solWallet = privyWallets[0];
|
||||||
|
|
||||||
if (!solWallet || !solWallet.address) {
|
if (!solWallet || !solWallet.address) {
|
||||||
throw new Error('No connected Solana wallet found.');
|
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 solAmount = (gpu.price_usd / (solPrice || 1)).toFixed(6);
|
const solAmount = (gpu.price_usd / (solPrice || 1)).toFixed(6);
|
||||||
|
|
||||||
const transaction = new Transaction().add(
|
const transaction = new Transaction().add(
|
||||||
@ -82,34 +104,41 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps)
|
|||||||
);
|
);
|
||||||
|
|
||||||
transaction.feePayer = fromPubKey;
|
transaction.feePayer = fromPubKey;
|
||||||
const { blockhash } = await connection.getRecentBlockhash();
|
const { blockhash } = await connection.getLatestBlockhash();
|
||||||
transaction.recentBlockhash = blockhash;
|
transaction.recentBlockhash = blockhash;
|
||||||
|
|
||||||
const signedTx = await signTransaction({
|
const signedTx = await solWallet.signTransaction(transaction);
|
||||||
transaction,
|
|
||||||
connection,
|
|
||||||
address: solWallet.address,
|
|
||||||
});
|
|
||||||
|
|
||||||
const txId = await connection.sendRawTransaction(signedTx.serialize());
|
const txId = await connection.sendRawTransaction(signedTx.serialize());
|
||||||
await connection.confirmTransaction(txId, 'confirmed');
|
await connection.confirmTransaction(txId, 'confirmed');
|
||||||
|
|
||||||
await axios.post('/api/orders', {
|
// Send confirmation email
|
||||||
gpuId: gpu.id,
|
await axios.post(EMAIL_API_URL, {
|
||||||
userEmail: user?.email?.address,
|
email: userEmail,
|
||||||
txSignature: txId,
|
product: gpu.title,
|
||||||
status: 'success',
|
price_hour: gpu.price_per_hour,
|
||||||
|
price: gpu.price_usd.toFixed(2),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Store order in Supabase
|
||||||
|
const { error } = await supabase.from('orders').insert([
|
||||||
|
{
|
||||||
|
gpu_id: gpu.id,
|
||||||
|
user_email: userEmail,
|
||||||
|
amount_sol: parseFloat(solAmount),
|
||||||
|
sol_tx_signature: txId,
|
||||||
|
status: 'success',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(`Failed to save order: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
setSuccessMsg(`Payment successful! Transaction ID: ${txId}`);
|
setSuccessMsg(`Payment successful! Transaction ID: ${txId}`);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setErrorMsg(err.message || 'Payment failed.');
|
setErrorMsg(err.message || 'Payment failed.');
|
||||||
await axios.post('/api/orders', {
|
|
||||||
gpuId: gpu.id,
|
|
||||||
userEmail: user?.email?.address,
|
|
||||||
txSignature: null,
|
|
||||||
status: 'failed',
|
|
||||||
});
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -139,22 +168,64 @@ export const GpuPaymentModal = ({ isOpen, onClose, gpu }: GpuPaymentModalProps)
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-6 flex justify-end gap-3">
|
{/* Wallet not connected */}
|
||||||
<button
|
{privyWallets.length === 0 ? (
|
||||||
onClick={onClose}
|
<div className="mt-6 text-center">
|
||||||
disabled={loading}
|
<p className="text-sm text-gray-700 mb-4">
|
||||||
className="px-4 py-2 rounded-lg border border-gray-300 text-gray-600 hover:bg-gray-100 transition"
|
To proceed with payment, please connect a Solana wallet.
|
||||||
>
|
</p>
|
||||||
Cancel
|
<button
|
||||||
</button>
|
onClick={connectWallet}
|
||||||
<button
|
className="px-4 py-2 rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 transition"
|
||||||
onClick={handlePayment}
|
>
|
||||||
disabled={loading}
|
Connect Wallet
|
||||||
className="px-4 py-2 rounded-lg bg-green-600 text-white hover:bg-green-700 transition disabled:opacity-60"
|
</button>
|
||||||
>
|
</div>
|
||||||
{loading ? 'Processing...' : 'Confirm Payment'}
|
) : (
|
||||||
</button>
|
<>
|
||||||
</div>
|
{/* Show email prompt if not logged in with email */}
|
||||||
|
{!user?.email?.address && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<label className="block text-sm text-gray-700 mb-1">
|
||||||
|
Email for invoice & access:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
value={emailInput}
|
||||||
|
onChange={(e) => setEmailInput(e.target.value)}
|
||||||
|
onBlur={() => setEmailTouched(true)}
|
||||||
|
className="w-full px-3 py-2 border rounded-md text-sm"
|
||||||
|
/>
|
||||||
|
{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>
|
||||||
);
|
);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user