This document outlines the implementation of three key features for the Stellar Market platform:
- Enhanced Freighter Wallet Connection - Improved wallet integration with session management
- Dispute Status Notifications - Real-time in-app and email notifications for dispute events
- Wallet Disconnect & Session Management - Secure session handling with automatic expiry
New Session Management:
- Session timeout: 30 minutes of inactivity
- Session warning: 5 minutes before expiry
- Automatic session extension on user activity
- Session persistence in localStorage
New State Properties:
isSessionActive: boolean; // Whether wallet session is active
sessionExpiresIn: number | null; // Milliseconds until session expires
extendSession: () => void; // Function to extend sessionSession Lifecycle:
- Connection: User connects wallet → Session created with timestamp
- Activity: Any wallet action (transaction, account switch) → Session extended
- Warning: 5 minutes before expiry →
stellarmarket:sessionWarningevent dispatched - Expiry: 30 minutes of inactivity → Auto-disconnect with
stellarmarket:sessionExpiredevent
Key Functions:
saveSession(address)- Creates new sessionupdateSessionActivity()- Extends session timeoutclearSession()- Clears session datagetStoredSession()- Retrieves stored session
Event Listeners:
freighter#accountChanged- Handles account switching with session updatefreighter#disconnected- Handles wallet disconnectionvisibilitychange- Re-verifies connection when tab becomes visible
import { useWallet } from "@/context/WalletContext";
export function WalletComponent() {
const {
address,
connect,
disconnect,
isSessionActive,
sessionExpiresIn,
extendSession
} = useWallet();
// Listen for session warning
useEffect(() => {
const handleSessionWarning = (e: Event) => {
const detail = (e as CustomEvent).detail;
console.log(`Session expires in ${detail.expiresIn}ms`);
// Show warning UI
};
window.addEventListener("stellarmarket:sessionWarning", handleSessionWarning);
return () => window.removeEventListener("stellarmarket:sessionWarning", handleSessionWarning);
}, []);
// Listen for session expiry
useEffect(() => {
const handleSessionExpired = () => {
console.log("Session expired - user disconnected");
// Show expiry message
};
window.addEventListener("stellarmarket:sessionExpired", handleSessionExpired);
return () => window.removeEventListener("stellarmarket:sessionExpired", handleSessionExpired);
}, []);
return (
<div>
{address ? (
<>
<p>Connected: {address}</p>
<p>Session Active: {isSessionActive}</p>
{sessionExpiresIn && (
<p>Expires in: {Math.round(sessionExpiresIn / 1000)}s</p>
)}
<button onClick={extendSession}>Extend Session</button>
<button onClick={disconnect}>Disconnect</button>
</>
) : (
<button onClick={connect}>Connect Wallet</button>
)}
</div>
);
}Dispute Service (backend/src/services/dispute.service.ts)
Notification Triggers:
-
Dispute Raised (
createDispute)- Sent to: Client and Freelancer
- Type:
DISPUTE_RAISED - Batching: Skipped (urgent)
- Email: Sent if preference enabled
-
Vote Cast (
castVote)- Sent to: Client and Freelancer
- Type:
DISPUTE_RAISED(vote update) - Message: Indicates vote choice (client/freelancer)
- Batching: Normal
-
Dispute Resolved (
resolveDispute)- Sent to: Client and Freelancer
- Type:
DISPUTE_RESOLVED - Batching: Skipped (urgent)
- Email: Sent with outcome details
Notification Service (backend/src/services/notification.service.ts)
Email Support:
- Added
DISPUTE_RESOLVEDevent type - Outcome metadata passed to email template
- Conditional sending based on
NotificationPreference
Email Templates:
-
Dispute Opened (
dispute-opened.ts)- Existing template
- Notifies about dispute initiation
- Includes action link to dispute details
-
Dispute Resolved (
dispute-resolved.ts) - NEW- Shows resolution outcome (client/freelancer)
- Indicates job completion
- Includes action link to job details
Notification Types:
enum NotificationType {
// ... existing types
DISPUTE_RAISED
DISPUTE_RESOLVED
// ... other types
}Notification Preferences:
model NotificationPreference {
userId String @id
emailEnabled Boolean @default(true)
emailDisputeOpened Boolean @default(true) // Covers both RAISED and RESOLVED
// ... other preferences
}Dispute Routes (backend/src/routes/dispute.routes.ts)
Existing endpoints automatically trigger notifications:
POST /api/disputes- Create disputePOST /api/disputes/:id/votes- Cast votePATCH /api/disputes/:id/resolve- Resolve dispute
Socket.IO Emissions:
// Emitted to user:${userId} room
io.to(`user:${userId}`).emit("notification:new", {
id: string;
userId: string;
type: "DISPUTE_RAISED" | "DISPUTE_RESOLVED";
title: string;
message: string;
metadata: {
disputeId: string;
jobId: string;
initiatorId?: string;
voterId?: string;
outcome?: string;
};
read: boolean;
createdAt: Date;
});// Frontend - Listen for dispute notifications
import { useSocket } from "@/context/SocketContext";
export function DisputeNotifications() {
const { socket } = useSocket();
useEffect(() => {
socket?.on("notification:new", (notification) => {
if (notification.type === "DISPUTE_RAISED") {
showAlert(`Dispute raised: ${notification.message}`);
} else if (notification.type === "DISPUTE_RESOLVED") {
showAlert(`Dispute resolved: ${notification.message}`);
}
});
return () => {
socket?.off("notification:new");
};
}, [socket]);
return null;
}Automatic Disconnect Triggers:
- User clicks "Disconnect" button
- Wallet extension is removed/disabled
- Wallet is locked
- Account access is revoked
- Session timeout (30 minutes)
Disconnect Flow:
User Action / Event
↓
Verify Wallet Status
↓
Clear Local State (address, balance, balances)
↓
Clear Storage (STORAGE_KEY, SESSION_KEY)
↓
Clear Timeouts (session timeout, warning)
↓
Dispatch "stellarmarket:walletDisconnected" Event
↓
Notify Other Components
State Cleanup:
const disconnect = useCallback(() => {
setAddress(null);
setError(null);
setBalance(null);
setBalances([]);
localStorage.removeItem(STORAGE_KEY);
clearSession();
window.dispatchEvent(new CustomEvent("stellarmarket:walletDisconnected"));
}, [clearSession]);Session Storage Format:
interface WalletSession {
address: string; // Connected wallet address
connectedAt: number; // Timestamp of connection
lastActivityAt: number; // Timestamp of last activity
}Session Timeout Logic:
Activity Detected
↓
Update lastActivityAt timestamp
↓
Clear existing timeouts
↓
Set warning timeout (25 minutes)
↓
Set expiry timeout (30 minutes)
↓
Dispatch warning event at 25 minutes
↓
Auto-disconnect at 30 minutes
Session Restoration:
App Mount
↓
Check localStorage for STORAGE_KEY
↓
Retrieve stored session
↓
Verify session age < 30 minutes
↓
Check Freighter installed
↓
Get current address
↓
Restore session if valid
Activities that Extend Session:
- Wallet connection
- Account switching
- Transaction signing
- Balance refresh
- Manual session extension
Implementation:
const updateSessionActivity = useCallback(() => {
const session = getStoredSession();
if (session) {
session.lastActivityAt = Date.now();
localStorage.setItem(SESSION_KEY, JSON.stringify(session));
setSessionExpiresIn(SESSION_TIMEOUT_MS);
// Clear and reset timeouts
if (sessionTimeoutId.current) clearTimeout(sessionTimeoutId.current);
if (sessionWarningId.current) clearTimeout(sessionWarningId.current);
// Set new timeouts
sessionWarningId.current = setTimeout(() => {
window.dispatchEvent(
new CustomEvent("stellarmarket:sessionWarning", {
detail: { expiresIn: SESSION_WARNING_MS },
}),
);
}, SESSION_TIMEOUT_MS - SESSION_WARNING_MS);
sessionTimeoutId.current = setTimeout(() => {
disconnect();
window.dispatchEvent(new CustomEvent("stellarmarket:sessionExpired"));
}, SESSION_TIMEOUT_MS);
}
}, [getStoredSession, disconnect]);- Session Timeout: 30 minutes prevents unauthorized access if device is left unattended
- Activity Tracking: Only legitimate user actions extend session
- Secure Storage: Session data stored in localStorage (not sensitive data)
- Event-Driven: Components can react to session events
- Automatic Cleanup: All timeouts and intervals cleared on disconnect
- Wallet Verification: Connection re-verified on tab visibility change
Graceful Degradation:
- If Freighter is not installed → Show "NOT_INSTALLED" error
- If wallet is locked → Show "LOCKED" error
- If connection fails → Show descriptive error message
- If session expires → Auto-disconnect with event notification
- Enhanced WalletContext with session management
- Session timeout (30 minutes)
- Session warning (5 minutes before expiry)
- Automatic session extension on activity
- Wallet disconnect with cleanup
- Event listeners for account changes
- Event listeners for wallet disconnection
- Visibility change handling
- Dispute notifications on creation
- Dispute notifications on vote cast
- Dispute notifications on resolution
- Email template for dispute opened
- Email template for dispute resolved
- Notification service integration
- Email service integration
- Metadata passing for outcomes
- NotificationType enum includes DISPUTE_RESOLVED
- NotificationPreference supports dispute emails
- Dispute model includes outcome field
Wallet Connection:
- Install Freighter extension
- Click "Connect Wallet"
- Verify address displays
- Verify balance loads
- Verify session is active
Session Management:
- Connect wallet
- Wait 25 minutes → Verify warning event
- Click "Extend Session" → Verify timeout resets
- Wait 30 minutes without activity → Verify auto-disconnect
- Verify session data cleared from localStorage
Wallet Disconnect:
- Connect wallet
- Click "Disconnect" → Verify state cleared
- Lock Freighter → Verify auto-disconnect
- Remove Freighter extension → Verify auto-disconnect
- Switch accounts → Verify session updated
Dispute Notifications:
- Create dispute → Verify in-app notification
- Check email → Verify dispute opened email
- Cast vote → Verify vote notification
- Resolve dispute → Verify resolution notification
- Check email → Verify dispute resolved email
// Example test
describe("WalletContext", () => {
it("should extend session on activity", async () => {
const { result } = renderHook(() => useWallet());
act(() => {
result.current.connect();
});
const initialExpiry = result.current.sessionExpiresIn;
act(() => {
result.current.extendSession();
});
expect(result.current.sessionExpiresIn).toBe(SESSION_TIMEOUT_MS);
});
it("should auto-disconnect after timeout", async () => {
jest.useFakeTimers();
const { result } = renderHook(() => useWallet());
act(() => {
result.current.connect();
});
expect(result.current.address).toBeTruthy();
act(() => {
jest.advanceTimersByTime(SESSION_TIMEOUT_MS + 1000);
});
expect(result.current.address).toBeNull();
jest.useRealTimers();
});
});No new environment variables required. Uses existing:
NEXT_PUBLIC_API_URL- Backend API URLNEXT_PUBLIC_FRONTEND_URL- Frontend URL (for email links)
Frontend (WalletContext.tsx):
const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
const SESSION_WARNING_MS = 5 * 60 * 1000; // 5 minutes
const STORAGE_KEY = "stellarmarket_wallet_connected";
const SESSION_KEY = "stellarmarket_wallet_session";Backend:
- Uses existing notification batching (5 seconds, max 10)
- Dispute notifications bypass batching (urgent)
- Check localStorage is enabled
- Verify SESSION_KEY is being set
- Check browser console for errors
- Verify NotificationPreference exists for user
- Check email configuration in backend
- Verify Socket.IO connection is active
- Check notification service logs
- Verify Freighter extension is responding
- Check browser console for errors
- Try manual disconnect button
- Clear localStorage and refresh
- Multi-wallet Support: Add support for other Stellar wallets
- Session Persistence: Option to remember session across browser restarts
- Biometric Authentication: Add fingerprint/face recognition for session extension
- Notification Preferences UI: Allow users to customize notification settings
- Dispute Analytics: Track dispute resolution times and outcomes
- Webhook Support: Send dispute notifications to external systems