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 */}
-
-