forked from StellarSplit/StellarSplit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathusePaymentFeed.ts
More file actions
136 lines (113 loc) · 5.26 KB
/
Copy pathusePaymentFeed.ts
File metadata and controls
136 lines (113 loc) · 5.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/**
* usePaymentFeed.ts
* Location: frontend/src/hooks/usePaymentFeed.ts
*
* Connects a socket.io-client socket to the backend PaymentGateway,
* subscribes to the active split room, and exposes live payment events.
*/
import { useCallback, useEffect, useRef, useState } from 'react';
import { io, type Socket } from 'socket.io-client';
// ─── Types ────────────────────────────────────────────────────────────────────
export type PaymentEventStatus =
| 'pending'
| 'processing'
| 'confirmed'
| 'failed';
export interface PaymentStatusEvent {
/** ISO timestamp of the event */
timestamp: string;
/** The participant / wallet that made the payment */
payerId: string;
/** Human-readable shortened address or name */
payerLabel: string;
/** Amount paid in the split's currency */
amount: number;
/** Currency code, e.g. "USDC" */
currency: string;
/** Stellar transaction hash */
txHash: string;
/** Current status of the payment */
status: PaymentEventStatus;
}
export interface SplitCompletionEvent {
splitId: string;
timestamp: string;
totalAmount: number;
currency: string;
}
export type FeedStatus = 'connecting' | 'connected' | 'disconnected' | 'error';
export interface UsePaymentFeedReturn {
/** Most recent payment-status-update event, or null if none received yet */
latestEvent: PaymentStatusEvent | null;
/** Fired once when all participants have settled */
completionEvent: SplitCompletionEvent | null;
/** Connection health */
status: FeedStatus;
/** All events received this session, newest first, capped at MAX_EVENTS */
events: PaymentStatusEvent[];
}
// ─── Constants ────────────────────────────────────────────────────────────────
const MAX_EVENTS = 10;
const SOCKET_URL =
import.meta.env.VITE_WS_URL ?? import.meta.env.VITE_API_BASE_URL ?? '';
// ─── Hook ─────────────────────────────────────────────────────────────────────
/**
* Subscribe to real-time payment updates for a given split.
*
* @param splitId - The split room to join. Pass null / undefined to skip.
* @param authToken - JWT used to authenticate the socket handshake.
*/
export function usePaymentFeed(
splitId: string | null | undefined,
authToken?: string | null,
): UsePaymentFeedReturn {
const socketRef = useRef<Socket | null>(null);
const [latestEvent, setLatestEvent] = useState<PaymentStatusEvent | null>(null);
const [completionEvent, setCompletionEvent] = useState<SplitCompletionEvent | null>(null);
const [status, setStatus] = useState<FeedStatus>('disconnected');
const [events, setEvents] = useState<PaymentStatusEvent[]>([]);
const pushEvent = useCallback((event: PaymentStatusEvent) => {
setLatestEvent(event);
setEvents((prev) => [event, ...prev].slice(0, MAX_EVENTS));
}, []);
useEffect(() => {
if (!splitId) return;
// ── Connect ──────────────────────────────────────────────────────────
const socket: Socket = io(SOCKET_URL, {
path: '/socket.io',
transports: ['websocket'],
auth: authToken ? { token: authToken } : undefined,
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
});
socketRef.current = socket;
setStatus('connecting');
// ── Lifecycle events ─────────────────────────────────────────────────
socket.on('connect', () => {
setStatus('connected');
socket.emit('join-room', { roomId: splitId });
});
socket.on('disconnect', () => {
setStatus('disconnected');
});
socket.on('connect_error', () => {
setStatus('error');
});
// ── Domain events ────────────────────────────────────────────────────
socket.on('payment-status-update', (payload: PaymentStatusEvent) => {
pushEvent(payload);
});
socket.on('split-completion', (payload: SplitCompletionEvent) => {
setCompletionEvent(payload);
});
// ── Cleanup ──────────────────────────────────────────────────────────
return () => {
socket.emit('leave-user-room', { roomId: splitId });
socket.disconnect();
socketRef.current = null;
setStatus('disconnected');
};
}, [splitId, authToken, pushEvent]);
return { latestEvent, completionEvent, status, events };
}