Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion pages/api/notifications/send-notification.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { SendNotificationSchema } from '../../../src/schemas/notification.schema.ts';

export default async function handler(req, res) {
if (req.method === 'GET') {
return res.status(200).json({
Expand All @@ -11,7 +13,15 @@ export default async function handler(req, res) {
}

try {
const { userId, title, body, url } = req.body;
const parsed = SendNotificationSchema.safeParse(req.body || {});
if (!parsed.success) {
return res.status(400).json({
error: 'Validation failed',
details: parsed.error.flatten().fieldErrors,
});
}

const { userId, title, body, url } = parsed.data;
const notificationId = `notif_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;

console.log('\n🚀 ========== SENDING NOTIFICATION ==========');
Expand Down
22 changes: 20 additions & 2 deletions pages/api/notifications/subscribe.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { subscriptions } from '../../../lib/subscriptions';
import {
SubscribeNotificationSchema,
UnsubscribeNotificationSchema,
} from '../../../src/schemas/notification.schema.ts';

export default function handler(req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
Expand All @@ -11,7 +15,14 @@ export default function handler(req, res) {

if (req.method === 'POST') {
try {
const subscription = req.body;
const parsed = SubscribeNotificationSchema.safeParse(req.body || {});
if (!parsed.success) {
return res.status(400).json({
error: 'Validation failed',
details: parsed.error.flatten().fieldErrors,
});
}
const subscription = parsed.data;
const userId = subscription.userId || 'anonymous';

subscriptions.set(userId, subscription);
Expand All @@ -30,7 +41,14 @@ export default function handler(req, res) {
}
} else if (req.method === 'DELETE') {
try {
const { userId } = req.body;
const parsed = UnsubscribeNotificationSchema.safeParse(req.body || {});
if (!parsed.success) {
return res.status(400).json({
error: 'Validation failed',
details: parsed.error.flatten().fieldErrors,
});
}
const { userId } = parsed.data;
subscriptions.delete(userId);
console.log('[Push] Unsubscribed user:', userId);
res.status(200).json({ success: true });
Expand Down
10 changes: 9 additions & 1 deletion pages/api/notifications/track.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { subscriptions } from '../../../lib/subscriptions';
import { TrackNotificationSchema } from '../../../src/schemas/notification.schema.ts';

let trackingLogs = [];

Expand All @@ -13,7 +14,14 @@ export default function handler(req, res) {

if (req.method === 'POST') {
try {
const { notificationId, event, userId, timestamp, message, title, error } = req.body;
const parsed = TrackNotificationSchema.safeParse(req.body || {});
if (!parsed.success) {
return res.status(400).json({
error: 'Validation failed',
details: parsed.error.flatten().fieldErrors,
});
}
const { notificationId, event, userId, timestamp, message, title, error } = parsed.data;

// Enhanced logging - shows full message body
console.log('\n========================================');
Expand Down
10 changes: 9 additions & 1 deletion src/components/web3/TransactionManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,23 @@ export const TransactionManager: React.FC<TransactionManagerProps> = ({

// Load history on mount
useEffect(() => {
if (typeof localStorage === 'undefined' || !wallet.address) return;
if (typeof localStorage === 'undefined') return;

if (!wallet.address) {
setTxHistory([]);
return;
}

const saved = localStorage.getItem(`tx_history_${wallet.address}`);
if (saved) {
try {
setTxHistory(JSON.parse(saved));
} catch (error) {
console.error('[TransactionManager] Failed to load history:', error);
setTxHistory([]);
}
} else {
setTxHistory([]);
}
}, [wallet.address]);

Expand Down
126 changes: 126 additions & 0 deletions src/components/web3/__tests__/TransactionManager.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import React from 'react';
import { render, screen, fireEvent, act } from '@testing-library/react';
import { describe, expect, it, vi, beforeEach } from 'vitest';
import { TransactionManager } from '../TransactionManager';
import { useWeb3Wallet } from '@/hooks/useWeb3Wallet';

vi.mock('@/hooks/useWeb3Wallet', () => ({
useWeb3Wallet: vi.fn(),
}));

describe('TransactionManager', () => {
const mockWallet = {
isConnected: true,
address: '0x1234567890123456789012345678901234567890',
provider: 'metamask',
chainId: '0x1',
supportedChains: {
'0x1': {
chainId: '0x1',
chainName: 'Ethereum Mainnet',
rpcUrl: 'https://eth.rpc',
explorerUrl: 'https://etherscan.io',
},
},
sendTransaction: vi.fn(),
};

beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
});

it('renders connect message when wallet is disconnected', () => {
vi.mocked(useWeb3Wallet).mockReturnValue({
...mockWallet,
isConnected: false,
address: null,
} as any);

render(<TransactionManager />);
expect(screen.getByText(/Connect wallet to manage transactions/i)).toBeInTheDocument();
});

it('loads and displays transaction history when connected', () => {
vi.mocked(useWeb3Wallet).mockReturnValue(mockWallet as any);

const mockHistory = [
{
hash: '0xmocktxhash123456789',
status: 'success',
timestamp: Date.now(),
from: mockWallet.address,
to: '0xrecipientaddress',
value: '1.5',
},
];

localStorage.setItem(`tx_history_${mockWallet.address}`, JSON.stringify(mockHistory));

render(<TransactionManager />);

expect(screen.getByText(/Recent Transactions/i)).toBeInTheDocument();
expect(screen.getByText(/1.5 ETH/i)).toBeInTheDocument();
});

it('clears transaction history state when wallet is disconnected', async () => {
vi.mocked(useWeb3Wallet).mockReturnValue(mockWallet as any);

const mockHistory = [
{
hash: '0xmocktxhash123456789',
status: 'success',
timestamp: Date.now(),
from: mockWallet.address,
to: '0xrecipientaddress',
value: '1.5',
},
];
localStorage.setItem(`tx_history_${mockWallet.address}`, JSON.stringify(mockHistory));

const { rerender } = render(<TransactionManager />);
expect(screen.getByText(/1.5 ETH/i)).toBeInTheDocument();

// Rerender with disconnected wallet
vi.mocked(useWeb3Wallet).mockReturnValue({
...mockWallet,
isConnected: false,
address: null,
} as any);

rerender(<TransactionManager />);
expect(screen.queryByText(/Recent Transactions/i)).not.toBeInTheDocument();
expect(screen.queryByText(/1.5 ETH/i)).not.toBeInTheDocument();
});

it('resets transaction history when switching to a wallet with no history', () => {
// Start with Wallet A containing history
vi.mocked(useWeb3Wallet).mockReturnValue(mockWallet as any);

const mockHistoryA = [
{
hash: '0xmocktxhashwalletA',
status: 'success',
timestamp: Date.now(),
from: mockWallet.address,
to: '0xrecipient',
value: '1.5',
},
];
localStorage.setItem(`tx_history_${mockWallet.address}`, JSON.stringify(mockHistoryA));

const { rerender } = render(<TransactionManager />);
expect(screen.getByText(/1.5 ETH/i)).toBeInTheDocument();

// Switch to Wallet B (which has no saved history in localStorage)
const walletAddressB = '0x9876543210987654321098765432109876543210';
vi.mocked(useWeb3Wallet).mockReturnValue({
...mockWallet,
address: walletAddressB,
} as any);

rerender(<TransactionManager />);
expect(screen.queryByText(/1.5 ETH/i)).not.toBeInTheDocument();
expect(screen.queryByText(/Recent Transactions/i)).not.toBeInTheDocument();
});
});
10 changes: 8 additions & 2 deletions src/hooks/__tests__/useWeb3Wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,17 @@ describe('useWeb3Wallet', () => {

const { result } = renderHook(() => useWeb3Wallet());

let connectPromise: Promise<any>;
await act(async () => {
await Promise.all([result.current.connect('metamask'), result.current.connect('metamask')]);
vi.runAllTimers();
connectPromise = Promise.all([
result.current.connect('metamask'),
result.current.connect('metamask'),
]);
await vi.runAllTimersAsync();
});

await connectPromise;

// FIFO: first call's eth_requestAccounts resolves before the second call starts
expect(callOrder[0]).toBeLessThan(callOrder[callOrder.length - 1]);
expect(result.current.isConnected).toBe(true);
Expand Down
1 change: 1 addition & 0 deletions src/hooks/useWeb3Wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { validateWalletInteraction, safeWalletCall } from '@/utils/web3/walletValidation';
import { walletCache, walletCacheKeys, CACHE_TTL } from '@/utils/web3/walletCache';
import { walletConnectionQueue } from '@/utils/web3/walletQueue';

/**
* Supported wallet providers
Expand Down
Loading
Loading