From 11cd5323ceff3cdfe9dc16e67222d4d4656c24a5 Mon Sep 17 00:00:00 2001 From: tali-creator Date: Tue, 7 Oct 2025 10:03:51 +0100 Subject: [PATCH] fix: split, notifcation toast --- components/auth/protected-route.tsx | 55 +- components/context/AuthContext.tsx | 432 +++++++++++-- components/dashboard/recent-activity.tsx | 58 +- components/dashboard/tabs/history.tsx | 2 +- components/dashboard/tabs/payment-split.tsx | 666 ++++---------------- components/dashboard/tabs/qr-payment.tsx | 453 +++++-------- components/dashboard/top-nav.tsx | 2 +- components/hooks/useAddresses.tsx | 18 +- components/hooks/useNotifications.ts | 50 +- components/hooks/useToastNotifications .tsx | 3 +- components/lib/utils.ts | 1 - components/modals/add-split.tsx | 380 +++++------ types/authContext.ts | 213 +++++++ 13 files changed, 1162 insertions(+), 1171 deletions(-) diff --git a/components/auth/protected-route.tsx b/components/auth/protected-route.tsx index 99a84c4..34bf437 100644 --- a/components/auth/protected-route.tsx +++ b/components/auth/protected-route.tsx @@ -1,7 +1,7 @@ "use client"; import { useAuth } from "@/components/context/AuthContext"; -import { useRouter } from "next/navigation"; +import { useRouter, usePathname } from "next/navigation"; import { useEffect, useState } from "react"; interface ProtectedRouteProps { @@ -11,34 +11,33 @@ interface ProtectedRouteProps { export default function ProtectedRoute({ children }: ProtectedRouteProps) { const { user, isLoading, token } = useAuth(); const router = useRouter(); - const [hasChecked, setHasChecked] = useState(false); + const pathname = usePathname(); + const [isChecking, setIsChecking] = useState(true); useEffect(() => { - // If we have a token but loading is taking too long, proceed cautiously - if (token && isLoading) { - const timeout = setTimeout(() => { - console.log('Auth check taking long, but we have a token - proceeding'); - setHasChecked(true); - }, 3000); // Reduced to 3 seconds - - return () => clearTimeout(timeout); + // If still loading auth state, wait + if (isLoading) { + return; } - // Normal flow: no token and not loading = redirect to auth - if (!isLoading && !token && !user) { - router.push("/auth"); - setHasChecked(true); + // Check if user is authenticated + const isAuthenticated = !!(token || user); + + if (isAuthenticated) { + setIsChecking(false); + } else { + // Only redirect if we're not already on the auth page + if (!pathname.includes('/auth')) { + console.log('Not authenticated - redirecting to auth'); + // Use replace: false to allow back button to work properly + router.push("/auth"); + } + setIsChecking(false); } + }, [user, isLoading, token, router, pathname]); - // Normal flow: we have a user = allow access - if (!isLoading && user) { - setHasChecked(true); - } - - }, [user, isLoading, token, router]); - - // Show loading spinner only briefly - if (isLoading && !hasChecked) { + // Show loading while checking authentication + if (isLoading || isChecking) { return (
@@ -46,7 +45,11 @@ export default function ProtectedRoute({ children }: ProtectedRouteProps) { ); } - // Allow access if we have a token (even if user profile fetch failed) - // OR if we have a user - return (token || user) ? <>{children} : null; + // Only render children if authenticated + if (token || user) { + return <>{children}; + } + + // Return null if not authenticated (will redirect) + return null; } \ No newline at end of file diff --git a/components/context/AuthContext.tsx b/components/context/AuthContext.tsx index 8ecbaaf..7adfa02 100644 --- a/components/context/AuthContext.tsx +++ b/components/context/AuthContext.tsx @@ -7,7 +7,7 @@ import React, { useEffect, ReactNode, } from "react"; -import { tokenManager } from "@/components/lib/api"; +import { tokenManager } from "@/components/lib/api"; import { useRouter } from "next/navigation"; import { WalletAddress, @@ -22,11 +22,23 @@ import { SendMoneyRequest, SendMoneyResponse, UnreadCountResponse, - UserProfile, ApiResponse + UserProfile, + ApiResponse, + CreateSplitPaymentRequest, + CreateSplitPaymentResponse, + ExecuteSplitPaymentResponse, + ExecutionHistoryResponse, + TemplatesResponse, + ToggleSplitPaymentResponse, + GenerateQRRequest, + GenerateQRResponse, + ParseQRRequest, + ParseQRResponse, + ExecuteQRRequest, + ExecuteQRResponse, + GetQRStatusResponse, } from "@/types/authContext"; - - interface AuthContextType { user: UserProfile | null; token: string | null; @@ -80,6 +92,22 @@ interface AuthContextType { checkDeposits: () => Promise; // Send transaction function sendTransaction: (request: SendMoneyRequest) => Promise; + createSplitPayment: ( + data: CreateSplitPaymentRequest + ) => Promise; + executeSplitPayment: (id: string) => Promise; + getSplitPaymentTemplates: (params?: { + status?: string; + }) => Promise; + getExecutionHistory: ( + id: string, + params?: { page?: number; limit?: number } + ) => Promise; + toggleSplitPaymentStatus: (id: string) => Promise; + generateQRCode: (data: GenerateQRRequest) => Promise; + parseQRCode: (data: ParseQRRequest) => Promise; + executeQRPayment: (data: ExecuteQRRequest) => Promise; + getQRPaymentStatus: (paymentId: string) => Promise; } const AuthContext = createContext(undefined); @@ -504,77 +532,76 @@ export const AuthProvider: React.FC = ({ children }) => { }; // Send transaction function - const sendTransaction = async ( - request: SendMoneyRequest -): Promise => { - if (!token) { - throw new Error("Authentication required to send transaction"); - } + const sendTransaction = async ( + request: SendMoneyRequest + ): Promise => { + if (!token) { + throw new Error("Authentication required to send transaction"); + } - try { - console.log("Sending transaction request:", request); + try { + console.log("Sending transaction request:", request); - const response = await fetch( - "https://velo-node-backend.onrender.com/wallet/send", - { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(request), - } - ); - - console.log("Response status:", response.status); - - // Handle non-OK responses - if (!response.ok) { - let errorMessage = `Failed to send transaction: ${response.status}`; - - try { - const errorData = await response.json(); - console.log("Error response data:", errorData); - - // Extract the most specific error message available - if (errorData.details) { - errorMessage = errorData.details; - } else if (errorData.error) { - errorMessage = errorData.error; - } else if (errorData.message) { - errorMessage = errorData.message; + const response = await fetch( + "https://velo-node-backend.onrender.com/wallet/send", + { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(request), } - } catch (parseError) { - console.log("Could not parse error response:", parseError); - // Use default error message if parsing fails + ); + + console.log("Response status:", response.status); + + // Handle non-OK responses + if (!response.ok) { + let errorMessage = `Failed to send transaction: ${response.status}`; + + try { + const errorData = await response.json(); + console.log("Error response data:", errorData); + + // Extract the most specific error message available + if (errorData.details) { + errorMessage = errorData.details; + } else if (errorData.error) { + errorMessage = errorData.error; + } else if (errorData.message) { + errorMessage = errorData.message; + } + } catch (parseError) { + console.log("Could not parse error response:", parseError); + // Use default error message if parsing fails + } + + // Create a proper Error object with the message + const error = new Error(errorMessage); + (error as any).response = { status: response.status }; + throw error; } - // Create a proper Error object with the message - const error = new Error(errorMessage); - (error as any).response = { status: response.status }; - throw error; - } + const data = await response.json(); + console.log("Transaction data:", data); + return data; + } catch (error) { + console.error("Error sending transaction:", error); - const data = await response.json(); - console.log("Transaction data:", data); - return data; - } catch (error) { - console.error("Error sending transaction:", error); - - // If it's already a properly formatted error, re-throw it - if (error instanceof Error) { - throw error; - } - - // Otherwise, create a new error - throw new Error( - typeof error === "string" - ? error - : "An unexpected error occurred while sending transaction" - ); - } -}; + // If it's already a properly formatted error, re-throw it + if (error instanceof Error) { + throw error; + } + // Otherwise, create a new error + throw new Error( + typeof error === "string" + ? error + : "An unexpected error occurred while sending transaction" + ); + } + }; // Notification functions const getNotifications = async ( @@ -882,6 +909,266 @@ export const AuthProvider: React.FC = ({ children }) => { } }; + const createSplitPayment = async ( + data: CreateSplitPaymentRequest + ): Promise => { + if (!token) { + throw new Error("Authentication required"); + } + + try { + const response = await fetch("https://velo-node-backend.onrender.com/split-payment/create", { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + throw new Error(`Failed to create split payment: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error("Error creating split payment:", error); + throw error; + } + }; + + const executeSplitPayment = async (id: string): Promise => { + if (!token) { + throw new Error("Authentication required"); + } + + try { + const response = await fetch(`https://velo-node-backend.onrender.com/split-payment/${id}/execute`, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`Failed to execute split payment: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error("Error executing split payment:", error); + throw error; + } + }; + + const getSplitPaymentTemplates = async (params?: { status?: string }): Promise => { + if (!token) { + throw new Error("Authentication required"); + } + + try { + const queryParams = params?.status ? `?status=${params.status}` : ''; + const response = await fetch(`https://velo-node-backend.onrender.com/split-payment/templates${queryParams}`, { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`Failed to get templates: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error("Error getting templates:", error); + throw error; + } + }; + + const getExecutionHistory = async ( + id: string, + params?: { page?: number; limit?: number } + ): Promise => { + if (!token) { + throw new Error("Authentication required"); + } + + try { + const urlParams = new URLSearchParams(); + if (params?.page) urlParams.append('page', params.page.toString()); + if (params?.limit) urlParams.append('limit', params.limit.toString()); + const query = urlParams.toString() ? `?${urlParams.toString()}` : ''; + const response = await fetch(`https://velo-node-backend.onrender.com/split-payment/${id}/executions${query}`, { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`Failed to get execution history: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error("Error getting execution history:", error); + throw error; + } + }; + + const toggleSplitPaymentStatus = async (id: string): Promise => { + if (!token) { + throw new Error("Authentication required"); + } + + try { + const response = await fetch(`https://velo-node-backend.onrender.com/split-payment/${id}/toggle`, { + method: "PATCH", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`Failed to toggle status: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error("Error toggling status:", error); + throw error; + } + }; + + + // QR Payment functions + const generateQRCode = async ( + data: GenerateQRRequest + ): Promise => { + if (!token) { + throw new Error("Authentication required to generate QR code"); + } + + try { + const response = await fetch( + "https://velo-node-backend.onrender.com/qr/generate", + { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + } + ); + + if (!response.ok) { + throw new Error(`Failed to generate QR code: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error("Error generating QR code:", error); + throw error; + } + }; + + const parseQRCode = async ( + data: ParseQRRequest + ): Promise => { + if (!token) { + throw new Error("Authentication required to parse QR code"); + } + + try { + const response = await fetch( + "https://velo-node-backend.onrender.com/qr/parse", + { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + } + ); + + if (!response.ok) { + throw new Error(`Failed to parse QR code: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error("Error parsing QR code:", error); + throw error; + } + }; + + const executeQRPayment = async ( + data: ExecuteQRRequest + ): Promise => { + if (!token) { + throw new Error("Authentication required to execute QR payment"); + } + + try { + const response = await fetch( + "https://velo-node-backend.onrender.com/qr/pay", + { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + } + ); + + if (!response.ok) { + throw new Error(`Failed to execute QR payment: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error("Error executing QR payment:", error); + throw error; + } + }; + + const getQRPaymentStatus = async ( + paymentId: string + ): Promise => { + if (!token) { + throw new Error("Authentication required to get QR payment status"); + } + + try { + const response = await fetch( + `https://velo-node-backend.onrender.com/qr/status/${paymentId}`, + { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + } + ); + + if (!response.ok) { + throw new Error(`Failed to get QR payment status: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error("Error getting QR payment status:", error); + throw error; + } + }; const value: AuthContextType = { user, token, @@ -905,6 +1192,15 @@ export const AuthProvider: React.FC = ({ children }) => { getTransactionHistory, checkDeposits, sendTransaction, + createSplitPayment, + executeSplitPayment, + getSplitPaymentTemplates, + getExecutionHistory, + toggleSplitPaymentStatus, + generateQRCode, + parseQRCode, + executeQRPayment, + getQRPaymentStatus, }; return {children}; diff --git a/components/dashboard/recent-activity.tsx b/components/dashboard/recent-activity.tsx index 0576fae..1e3a752 100644 --- a/components/dashboard/recent-activity.tsx +++ b/components/dashboard/recent-activity.tsx @@ -13,10 +13,41 @@ import { shortenAddress } from "../lib/utils"; export function RecentActivity({ activeTab }: DashboardProps) { const { notifications } = useNotifications(); + console.log("recent Noification", notifications); const filtered = notifications.filter((notif) => { - return notif.title === "Deposit Received"; + return notif.title === "Deposit Received" || notif.title === "Tokens Sent"; }); + // const getExplorerUrl = (txHash: string): string => { + // const explorerUrls: { [key: string]: { testnet: string; mainnet: string } } = { + // ethereum: { + // testnet: `https://sepolia.etherscan.io/tx/${txHash}`, + // mainnet: `https://etherscan.io/tx/${txHash}`, + // }, + // usdt_erc20: { + // testnet: `https://sepolia.etherscan.io/tx/${txHash}`, + // mainnet: `https://etherscan.io/tx/${txHash}`, + // }, + // bitcoin: { + // testnet: `https://blockstream.info/testnet/tx/${txHash}`, + // mainnet: `https://blockstream.info/tx/${txHash}`, + // }, + // solana: { + // testnet: `https://explorer.solana.com/tx/${txHash}?cluster=devnet`, + // mainnet: `https://explorer.solana.com/tx/${txHash}`, + // }, + // starknet: { + // testnet: `https://sepolia.voyager.online/tx/${txHash}`, + // mainnet: `https://voyager.online/tx/${txHash}`, + // }, + // }; + + // const explorer = explorerUrls[selectedToken]; + // if (!explorer) return "#"; + + // return currentNetwork === "testnet" ? explorer.testnet : explorer.mainnet; + // }; + const finalNotificationFix = filtered.slice(0, 5); return ( @@ -48,7 +79,7 @@ export function RecentActivity({ activeTab }: DashboardProps) {
)} - {notification.title === "Send Funds" && ( + {notification.title === "Tokens Sent" && (
@@ -62,10 +93,25 @@ export function RecentActivity({ activeTab }: DashboardProps) {
-
- {notification.details.amount} -
-
{shortenAddress(notification.details.address, 6)}
+ {notification.title === "Deposit Received" && ( +
+ {notification.details.amount} +
+ )} + + {notification.title === "Tokens Sent" && ( +
+ {notification.details.amount} +
+ )} + + {notification.title === "Deposit Received" && ( +
{shortenAddress(notification.details.address, 6)}
+ )} + + {notification.title === "Tokens Sent" && ( +
{shortenAddress(notification.details.txHash, 6)}
+ )}
))} diff --git a/components/dashboard/tabs/history.tsx b/components/dashboard/tabs/history.tsx index 88924ea..08b5be1 100644 --- a/components/dashboard/tabs/history.tsx +++ b/components/dashboard/tabs/history.tsx @@ -106,7 +106,7 @@ export default function History() { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - +console.log("transactions", transactions) useEffect(() => { setSearchQuery("") const fetchTransactions = async () => { diff --git a/components/dashboard/tabs/payment-split.tsx b/components/dashboard/tabs/payment-split.tsx index e6e6bc8..213b1cf 100644 --- a/components/dashboard/tabs/payment-split.tsx +++ b/components/dashboard/tabs/payment-split.tsx @@ -8,592 +8,156 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Check, Plus, AlertCircle, ChevronDown, Users, Loader2 } from "lucide-react"; import React, { useCallback, useState, useEffect } from "react"; -import { SplitData } from "@/splits"; -import { CallData, uint256 } from "starknet"; -import { useProvider } from "@starknet-react/core"; -import { useWalletAddresses } from "@/components/hooks/useAddresses"; -import { TOKEN_ADDRESSES as tokenAddress } from "autoswap-sdk"; -import { useMemo } from 'react'; - -// Fixed custom transaction sender with proper typing -const useCustomTransactionSender = (starknetAddress: string | null) => { - const sendTransaction = useMemo(() => { - if (!starknetAddress) return null; - - return async (calls: any[]) => { - try { - // Your custom transaction logic here - console.log(calls) - return { - transaction_hash: "0x" + Math.random().toString(16).substr(2, 8) - }; - } catch (error) { - throw error; - } - }; - }, [starknetAddress]); - - return { sendAsync: sendTransaction }; -}; - -// Token contract addresses -const TOKEN_ADDRESSES: { [key: string]: string } = { - USDT: tokenAddress.USDT, - USDC: tokenAddress.USDC || "", - STRK: tokenAddress.STRK || "", - ETH: tokenAddress.ETH || "", -}; - -// Token decimals for u256 conversion -const TOKEN_DECIMALS: { [key: string]: number } = { - USDT: 6, - USDC: 6, - STRK: 18, - ETH: 18, -}; - -// Event extraction helper function -const extractSmeIdFromEvents = (events: any[], recipientCount: number): string | null => { - if (!events || !Array.isArray(events)) { - console.error("No events array found", recipientCount); - return null; - } - - console.log("Raw events:", events); - - for (const event of events) { - if (event && ( - (event.name && ( - event.name.includes("Sme3Created") || - event.name.includes("Sme4Created") || - event.name.includes("Sme5Created") - )) || - (event.keys && event.keys.some((key: string) => - key.includes("sme") || key.includes("Sme") - )) - )) { - console.log("Found SME event:", event); - - if (event.data && event.data.length > 0) { - const smeId = event.data[0]; - if (smeId) { - return smeId.toString(); - } - } - - if (event.keys && event.keys.length > 0) { - for (const key of event.keys) { - if (key && key.toString().length > 10) { - return key.toString(); - } - } - } - } - } - - return null; -}; +import { useAuth } from "@/components/context/AuthContext"; export default function PaymentSplit() { + const { getSplitPaymentTemplates, executeSplitPayment, toggleSplitPaymentStatus } = useAuth(); + const [templates, setTemplates] = useState([]); + const [loading, setLoading] = useState(false); const [addSplitModal, setAddSplitModal] = useState(false); - const [splitData, setSplitData] = useState(null); - const [token, setToken] = useState("STRK"); - const [isCreating, setIsCreating] = useState(false); - const [isDistributing, setIsDistributing] = useState(false); - const [error, setError] = useState(""); - const [smeId, setSmeId] = useState(null); - const [starknetAddress, setStarknetAddress] = useState(null); - - const { provider } = useProvider(); - const { addresses, loading: addressesLoading, error: addressesError } = useWalletAddresses(); - console.log(addresses); - - // Get contract address from environment variables - const contractAddress = process.env.NEXT_PUBLIC_CONTRACT_ADDRESS; - if (!contractAddress) { - throw new Error("NEXT_PUBLIC_CONTRACT_ADDRESS is not defined"); - } - - // Hooks for sending transactions - const { sendAsync: sendCreateSme } = useCustomTransactionSender(starknetAddress); - const { sendAsync: sendDistributePayment } = useCustomTransactionSender(starknetAddress); - - // Fixed: Added proper null checks for splitData and recipients - const canPerformTransactions = starknetAddress && - splitData?.recipients && - Array.isArray(splitData.recipients) && - splitData.recipients.length > 0; - // Extract Starknet address from backend wallets - useEffect(() => { - if (addresses && addresses.length > 0) { - const starknetWallet = addresses.find(addr => - addr.chain?.toLowerCase() === 'starknet' && addr.address - ); - - if (starknetWallet) { - setStarknetAddress(starknetWallet.address); - console.log("Using Starknet address:", starknetWallet.address); - } else { - setError("No Starknet wallet address found in your account"); - } - } - }, [addresses]); - - const handleShowSplitModal = () => { - if (!starknetAddress) { - setError("Please connect your Starknet wallet first"); - return; + const refetchTemplates = async () => { + setLoading(true); + try { + const data = await getSplitPaymentTemplates(); + setTemplates(data.templates || []); + } catch (error) { + console.error("Failed to fetch templates:", error); + } finally { + setLoading(false); } - setAddSplitModal(!addSplitModal); }; - const totalPercentage = - splitData?.recipients?.reduce((total, recipient) => { - return total + (recipient.percentage || 0); - }, 0) || 0; - - const totalAmount = - splitData?.recipients?.reduce((total, recipient) => { - return total + parseFloat(recipient.amount || "0"); - }, 0) || 0; - - const tokens = ["USDT", "USDC", "STRK", "ETH"]; - - const handleTokenChange = useCallback((tkn: string) => { - setToken(tkn); + useEffect(() => { + refetchTemplates(); }, []); - // Create split on the smart contract - const handleCreateSplit = async () => { - if (!starknetAddress) { - setError("Starknet wallet address not available"); - return; - } - - if (!splitData || !splitData.recipients || splitData.recipients.length === 0) { - setError("Please provide split data with recipients"); - return; - } - - if (totalPercentage !== 100) { - setError("Total percentage must equal 100%"); - return; - } - - const recipientCount = splitData.recipients.length; - if (recipientCount < 3 || recipientCount > 5) { - setError("Split must have between 3-5 recipients"); - return; - } - - // Fixed: Check if sendCreateSme is null before calling - if (!sendCreateSme) { - setError("Transaction sender not available"); - return; - } - - setIsCreating(true); - setError(""); - + const handleExecute = async (id: string) => { try { - const functionName = `create_sme${recipientCount}`; - const params: any[] = []; - - // Add all recipients with proper null checks - for (let i = 0; i < recipientCount; i++) { - const recipient = splitData.recipients[i]; - if (!recipient) { - throw new Error(`Recipient at index ${i} is missing`); - } - if (!recipient.walletAddress) { - throw new Error(`Recipient ${recipient.name || i} is missing wallet address`); - } - if (!recipient.percentage) { - throw new Error(`Recipient ${recipient.name || i} is missing percentage`); - } - - params.push(recipient.walletAddress); - params.push(recipient.percentage); - } - - const call = { - contractAddress, - entrypoint: functionName, - calldata: CallData.compile(params), - }; - - const result = await sendCreateSme([call]); - - // Wait for transaction confirmation - await new Promise(resolve => setTimeout(resolve, 10000)); - - // Get transaction receipt - const receipt = await provider.getTransactionReceipt(result.transaction_hash); - - // Access events safely - const events = (receipt as any).events || - (receipt as any).transaction_receipt?.events || - (receipt as any).receipt?.events || []; - - console.log("Transaction receipt:", receipt); - - // Extract SME ID - const extractedSmeId = extractSmeIdFromEvents(events, recipientCount); - - if (!extractedSmeId) { - console.warn("Could not extract SME ID from events, using transaction hash as fallback"); - setSmeId(result.transaction_hash); - } else { - setSmeId(extractedSmeId); - } - - alert(`Split created successfully! ID: ${extractedSmeId || result.transaction_hash}`); - } catch (err) { - console.error("Failed to create split:", err); - setError("Failed to create split: " + (err as Error).message); - } finally { - setIsCreating(false); + await executeSplitPayment(id); + refetchTemplates(); + } catch (error) { + console.error("Failed to execute:", error); } }; - // Distribute payment to recipients - const handleDistributeSplit = async () => { - if (!starknetAddress) { - setError("Starknet wallet address not available"); - return; - } - - if (!splitData || !smeId) { - setError("Please create the split first"); - return; - } - - if (!totalAmount || isNaN(totalAmount) || totalAmount <= 0) { - setError("Please enter a valid amount to distribute"); - return; - } - - const tokenAddress = TOKEN_ADDRESSES[token]; - if (!tokenAddress) { - setError("Invalid token selected"); - return; - } - - // Fixed: Check if sendDistributePayment is null before calling - if (!sendDistributePayment) { - setError("Transaction sender not available"); - return; - } - - setIsDistributing(true); - setError(""); - + const handleToggle = async (id: string) => { try { - const decimals = TOKEN_DECIMALS[token] || 18; - const amountBN = BigInt(Math.floor(totalAmount * 10 ** decimals)); - const amountU256 = uint256.bnToUint256(amountBN); - - const recipientCount = splitData.recipients?.length || 0; - const functionName = `distribute_sme${recipientCount}_payment`; - - // Approve the contract to spend tokens - const approveCall = { - contractAddress: tokenAddress, - entrypoint: "approve", - calldata: CallData.compile({ - spender: contractAddress, - amount: amountU256, - }), - }; - - // Distribute payment using the SME ID - const distributeCall = { - contractAddress, - entrypoint: functionName, - calldata: CallData.compile([ - smeId, - amountU256, - tokenAddress, - ]), - }; - - await sendDistributePayment([approveCall, distributeCall]); - alert("Payment distributed successfully!"); - setSplitData(null); - setSmeId(null); - } catch (err) { - console.error("Failed to distribute payment:", err); - setError("Failed to distribute payment: " + (err as Error).message); - } finally { - setIsDistributing(false); + await toggleSplitPaymentStatus(id); + refetchTemplates(); + } catch (error) { + console.error("Failed to toggle:", error); } }; - // Clear error after some time - React.useEffect(() => { - if (error) { - const timer = setTimeout(() => { - setError(""); - }, 7000); - return () => clearTimeout(timer); - } - }, [error]); - - // Show loading state while fetching addresses - if (addressesLoading) { - return ( -
-
- -

Loading wallet addresses...

-
-
- ); - } - - // Show error if no Starknet address found - if (addressesError || !starknetAddress) { - return ( -
- - - Error - - {addressesError || "No Starknet wallet address found. Please make sure you have a Starknet wallet connected to your account."} - - -
- ); - } + const handleShowSplitModal = () => { + setAddSplitModal(true); + }; return ( -
- {/* Header */} -
-

- Payment Split -

-

- Split payments between multiple recipients automatically -

+
+
+

Split Payments

+
- {error && ( - - - Error - {error} - - )} - - {smeId && ( - - - Success - - Split created successfully! SME ID: {smeId.slice(0, 10)}... - - - )} - -
- {/* Main Content */} -
- {splitData ? ( - - -
- {splitData.title} -

{splitData.description}

-
- + {loading ? ( +
+ +
+ ) : templates.length === 0 ? ( + + +

+ No split created yet. Create your first payment split to get started. +

+
+ ) : ( +
+ {templates.map((template) => ( + + + {template.title} - - {/* Stats Cards */} -
- -
- -

Recipients

-
-

{splitData.recipients?.length || 0}

-
- -
- -

Total Percentage

-
-

{totalPercentage}%

-
- -
- -

Total Amount

-
-

- {totalAmount.toLocaleString()} {token} -

-
-
- - {/* Recipients List */} -
-

Recipients

-
- {splitData.recipients?.map((recipient, id) => ( - -
-
-

{recipient.name}

-

- {shortenAddress(recipient.walletAddress as `0x${string}`, 6)} -

-
-
-
-

- {parseFloat(recipient.amount).toLocaleString()} {token} -

-

- {recipient.percentage}% -

-
-
-
-
- ))} + +

{template.description}

+
+
+ Chain: + {template.chain.toUpperCase()}
-
- - {/* Action Buttons */} - {canPerformTransactions && ( -
- {!smeId ? ( - - ) : ( - - )} - -
- - - - - - {tokens.map((tkn) => ( - handleTokenChange(tkn)} - className="cursor-pointer" - > - {tkn} - - ))} - - -
+
+ Total Amount: + {template.totalAmount} +
+
+ Recipients: + {template.recipientCount}
- )} +
+ Executions: + {template.executionCount} +
+
+ Status: + {template.status} +
+
+
+ {template.canExecute && ( + + )} + +
- ) : ( - - -

- No split created yet. Create your first payment split to get started. -

- -
- )} + ))}
+ )} - {/* Sidebar */} -
- -

How It Works

-
-
-
- 1 -
-
-

Create Split

-

Add 3-5 recipients with their wallet addresses and percentages

-
+
+ +

How It Works

+
+
+
+ 1
-
-
- 2 -
-
-

Deploy Contract

-

Create the split on Starknet blockchain

-
+
+

Create Split

+

Add multiple recipients with their wallet addresses and amounts

-
-
- 3 -
-
-

Distribute Funds

-

Send payments that automatically split between recipients

-
+
+
+
+ 2 +
+
+

Save Template

+

Create reusable template on the backend

- - - -
+
+
+ 3 +
+
+

Execute Payments

+

Distribute funds to all recipients in one go

+
+
+
+
{addSplitModal && ( - + )}
); diff --git a/components/dashboard/tabs/qr-payment.tsx b/components/dashboard/tabs/qr-payment.tsx index f70ec42..88fff3c 100644 --- a/components/dashboard/tabs/qr-payment.tsx +++ b/components/dashboard/tabs/qr-payment.tsx @@ -2,13 +2,13 @@ import { Card } from "@/components/ui/Card"; import { ChevronDown, Loader2 } from "lucide-react"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState, useRef } from "react"; import QRCodeLib from "qrcode"; import Image from "next/image"; import { useWalletAddresses } from "@/components/hooks/useAddresses"; import useExchangeRates from "@/components/hooks/useExchangeRate"; -import { usePaymentMonitor } from "@/components/hooks/usePaymentMonitor"; import { QRCodeDisplay } from "@/components/modals/qr-code-display"; +import { useAuth } from "@/components/context/AuthContext"; // Utility function to normalize Starknet addresses const normalizeStarknetAddress = (address: string, chain: string): string => { @@ -20,51 +20,23 @@ const normalizeStarknetAddress = (address: string, chain: string): string => { return address; }; -// Generate proper URI schemes for different cryptocurrencies -const generateCryptoURI = (chain: string, address: string, amount: string,): string => { - const normalizedAmount = amount === "0" ? "0" : amount; - - switch (chain.toLowerCase()) { - case 'ethereum': - case 'eth': - case 'usdt_erc20': - // EIP-681 format for Ethereum and ERC20 tokens - return `ethereum:${address}?value=${normalizedAmount}`; - - case 'starknet': - case 'strk': - case 'usdt': - case 'usdc': - // Starknet uses similar format to Ethereum - return `starknet:${address}?value=${normalizedAmount}`; - - case 'bitcoin': - case 'btc': - // BIP-21 format for Bitcoin - return `bitcoin:${address}?amount=${normalizedAmount}`; - - case 'solana': - case 'sol': - // Solana URI scheme - return `solana:${address}?amount=${normalizedAmount}`; - - default: - // Fallback: just the address - return address; - } -}; - export default function QrPayment() { const [token, setToken] = useState("STRK"); const [amount, setAmount] = useState(""); - const [tokenWei, setTokenWei] = useState(BigInt(0)); const [showTokenDropdown, setShowTokenDropdown] = useState(false); const [showQR, setShowQR] = useState(false); const [qrData, setQrData] = useState(""); const [isProcessing, setIsProcessing] = useState(false); const [loading, setLoading] = useState(true); + const [paymentId, setPaymentId] = useState(""); + const [localPaymentStatus, setLocalPaymentStatus] = useState<"idle" | "pending" | "success" | "error">("idle"); + const [localError, setLocalError] = useState(null); const { addresses, loading: addressesLoading } = useWalletAddresses(); + const { rates, isLoading: ratesLoading, lastUpdated } = useExchangeRates(); + const { generateQRCode, getQRPaymentStatus } = useAuth(); + + const pollingRef = useRef(null); useEffect(() => { if (addresses && addresses.length > 0) { @@ -74,302 +46,165 @@ export default function QrPayment() { } }, [addresses, addressesLoading]); - const STARKNET_TOKEN_ADDRESSES = useMemo( - () => ({ - USDT: "0x068f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8", - USDC: "0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8", - STRK: "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", - }), - [] - ); - - const getDecimals = (selectedToken: string): number => { - return selectedToken === "STRK" || selectedToken === "ETH" ? 18 : 6; - }; - - const getReceiverAddress = useCallback((): string => { - if (!addresses || addresses.length === 0) return ""; - - if (["USDT", "USDC", "STRK"].includes(token)) { - const starknetAddr = addresses[3]?.address || ""; - return normalizeStarknetAddress( - starknetAddr, - addresses[3]?.chain || "STRK" - ); - } - - const addressMap: { [key: string]: number } = { - ETH: 0, - BTC: 1, - SOL: 2, - }; - - const index = addressMap[token]; - if (index !== undefined && addresses[index]) { - return normalizeStarknetAddress( - addresses[index].address, - addresses[index].chain - ); - } - - return ""; - }, [addresses, token]); + const tokens = useMemo(() => ["ETH", "BTC", "SOL", "STRK"], []); const getTokenChain = useCallback((): string => { const chainMap: { [key: string]: string } = { ETH: "ethereum", - BTC: "bitcoin", + BTC: "bitcoin", SOL: "solana", STRK: "starknet", - USDT: "starknet", - USDC: "starknet", }; return chainMap[token] || "ethereum"; }, [token]); - const getTokenAddress = useCallback((): string => { - if ( - STARKNET_TOKEN_ADDRESSES[token as keyof typeof STARKNET_TOKEN_ADDRESSES] - ) { - return STARKNET_TOKEN_ADDRESSES[ - token as keyof typeof STARKNET_TOKEN_ADDRESSES - ]; - } - return getReceiverAddress(); - }, [token, STARKNET_TOKEN_ADDRESSES, getReceiverAddress]); - - const currentReceiverAddress = getReceiverAddress(); - const currentTokenAddress = getTokenAddress(); - const currentChain = getTokenChain(); - - const { paymentStatus, error } = usePaymentMonitor({ - expectedAmount: tokenWei, - receiverAddress: currentReceiverAddress, - tokenAddress: currentTokenAddress, - enabled: !!qrData && !!currentReceiverAddress && !!currentTokenAddress, - pollInterval: 10000, - }); - - const { rates, isLoading: ratesLoading } = useExchangeRates(); - - useEffect(() => { - if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) { - setTokenWei(BigInt(0)); - return; - } - - const tokenPriceInNGN = rates[token as keyof typeof rates]; - if (!tokenPriceInNGN) { - setTokenWei(BigInt(0)); - return; - } - - const ngnAmount = Number.parseFloat(amount); - const tokenAmount = ngnAmount / tokenPriceInNGN; - const decimals = getDecimals(token); - const amountInWei = BigInt(Math.floor(tokenAmount * 10 ** decimals * 1)); - setTokenWei(amountInWei); - }, [amount, token, rates]); - - const calculateTokenAmount = useCallback(() => { - if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) { - return "0"; - } + const currentReceiverAddress = useMemo((): string => { + if (!addresses || addresses.length === 0) return ""; - if (ratesLoading && !rates[token as keyof typeof rates]) { - return "Loading..."; - } + const chain = getTokenChain(); + const addr = addresses.find(a => a.chain === chain); + if (!addr) return ""; - const ngnAmount = Number.parseFloat(amount); - const tokenPriceInNGN = rates[token as keyof typeof rates] || 1; - const tokenAmount = ngnAmount / tokenPriceInNGN; - const displayDecimals = getDecimals(token) === 18 ? 6 : 9; + return normalizeStarknetAddress(addr.address, chain); + }, [addresses, getTokenChain]); - return tokenAmount.toFixed(displayDecimals); - }, [amount, token, rates, ratesLoading]); + const calculateTokenAmount = useCallback((): string => { + const ngnAmount = parseFloat(amount) || 0; + const rate = rates[token] || 1; + const tokenAmount = ngnAmount / rate; + return tokenAmount.toFixed(6); + }, [amount, rates, token]); - const handleGenerateQR = useCallback(async () => { - if (!amount || isNaN(Number(amount)) || Number(amount) <= 0) { - alert("Please enter a valid amount"); - return; - } - - if (!currentReceiverAddress) { - alert("Receiver address not available. Please ensure wallet addresses are loaded."); - return; - } - - if (tokenWei === 0n) { - alert("Invalid token amount calculation"); - return; - } + const handleGenerateQR = async () => { + if (!currentReceiverAddress) return; setIsProcessing(true); - try { - const tokenAmount = calculateTokenAmount(); - - // Generate proper cryptocurrency URI for QR code - const cryptoURI = generateCryptoURI(currentChain, currentReceiverAddress, tokenAmount, ); - - console.log("Generating QR for:", { - chain: currentChain, - address: currentReceiverAddress, - amount: tokenAmount, - token: token, - uri: cryptoURI - }); + const cryptoAmount = calculateTokenAmount(); - // Generate QR code with the proper cryptocurrency URI - const qrCodeDataUrl = await QRCodeLib.toDataURL(cryptoURI, { + const request = { + chain: getTokenChain(), + network: "testnet", + amount: cryptoAmount, + description: "Payment request", + expiresInMinutes: 30, + }; + + const response = await generateQRCode(request); + + const qrImage = await QRCodeLib.toDataURL(response.qrCodeString, { width: 300, - margin: 2, - color: { - dark: "#000000", - light: "#FFFFFF", - }, - errorCorrectionLevel: 'M' + margin: 1, }); - setQrData(qrCodeDataUrl); + setQrData(qrImage); + setPaymentId(response.qrData.paymentId); setShowQR(true); + setLocalPaymentStatus("pending"); } catch (error) { - console.error("Error creating payment:", error); - alert("Failed to create payment request"); + console.error("Error generating QR:", error); + // Handle error, perhaps show toast } finally { setIsProcessing(false); } - }, [amount, currentReceiverAddress, currentChain, token, tokenWei, calculateTokenAmount]); - - const handleTokenSelect = useCallback((tkn: string) => { - setToken(tkn); - setShowTokenDropdown(false); - }, []); + }; - const handleAmountChange = useCallback( - (e: React.ChangeEvent) => { - const value = e.target.value; - if (/^\d*\.?\d*$/.test(value)) { - setAmount(value); + useEffect(() => { + if (!showQR || !paymentId) { + if (pollingRef.current) { + clearInterval(pollingRef.current); + pollingRef.current = null; } - }, - [] - ); - - const handleCloseQR = useCallback(() => { - setShowQR(false); - setQrData(""); - setTokenWei(0n); - }, []); - - const tokens = ["USDT", "USDC", "STRK", "ETH", "BTC", "SOL"]; + return; + } - useEffect(() => { - const handleClickOutside = () => { - if (showTokenDropdown) { - setShowTokenDropdown(false); + const pollStatus = async () => { + try { + const statusResponse = await getQRPaymentStatus(paymentId); + const { paymentStatus } = statusResponse; + + if (paymentStatus.status === "confirmed" || paymentStatus.status === "completed") { + setLocalPaymentStatus("success"); + if (pollingRef.current) clearInterval(pollingRef.current); + } else if (paymentStatus.isExpired) { + setLocalPaymentStatus("error"); + setLocalError("Payment request has expired"); + if (pollingRef.current) clearInterval(pollingRef.current); + } else { + setLocalPaymentStatus("pending"); + } + } catch (error) { + console.error("Error checking payment status:", error); } }; - document.addEventListener("click", handleClickOutside); + pollStatus(); // Initial check + pollingRef.current = setInterval(pollStatus, 5000); + return () => { - document.removeEventListener("click", handleClickOutside); + if (pollingRef.current) { + clearInterval(pollingRef.current); + pollingRef.current = null; + } }; - }, [showTokenDropdown]); + }, [showQR, paymentId, getQRPaymentStatus]); + + const handleCloseQR = () => { + setShowQR(false); + setQrData(""); + setPaymentId(""); + setLocalPaymentStatus("idle"); + setLocalError(null); + }; + + const handleTokenSelect = (tkn: string) => { + setToken(tkn); + setShowTokenDropdown(false); + }; const steps = [ { - step: "Create Payment Request", - description: "Enter Amount And Select Currency To Create Payment Request", + step: "Enter Amount", + description: "Specify the amount in NGN you want to receive", }, { - step: "Generate QR Code", - description: "QR code is generated after payment request is created", + step: "Generate QR", + description: "Create a unique payment request QR code", }, { - step: "Customer Scans", - description: "Customer Uses Wallet To Scan QR Code", + step: "Share QR", + description: "Send the QR code or address to the payer", }, { - step: "Payment Confirmed", - description: "Transaction Is Processed And Confirmed Automatically", + step: "Receive Payment", + description: "Funds will be credited after confirmation", }, ]; - if (loading || addressesLoading) { - return ( -
-
- -

Loading wallet addresses...

-
-
- ); - } - - if (!addresses || addresses.length === 0) { - return ( -
- -

No Wallet Addresses

-

- Unable to retrieve wallet addresses. Please check your connection and try again. -

-
-
- ); - } - return ( -
- {/* Header */} -
-

- QR Payment Generator -

-

- Create payment requests and generate QR codes for customers -

-
- -
- {/* Payment Form */} - -
- {/* Amount Input */} -
- -
- -
- ≈ {calculateTokenAmount()} {token} -
-
-
+
+
+ +

+ Create Payment Request +

- {/* Token Selector */} -
-