@silentswap/react 0.0.79 → 0.0.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,632 +1,47 @@
1
- import { useCallback, useEffect, useRef, useState } from 'react';
2
1
  /**
3
- * Output stage enum matching Svelte's OutputStage (string values)
4
- */
5
- export var OutputStage;
6
- (function (OutputStage) {
7
- OutputStage["NONE"] = "NONE";
8
- OutputStage["INIT"] = "INIT";
9
- OutputStage["FUNDED"] = "FUNDED";
10
- OutputStage["REDEEMED"] = "REDEEMED";
11
- OutputStage["IBC_SENT"] = "IBC_SENT";
12
- OutputStage["IBC_RCVD"] = "IBC_RCVD";
13
- OutputStage["BRIDGE_SENT"] = "BRIDGE_SENT";
14
- OutputStage["BRIDGE_CFRM"] = "BRIDGE_CFRM";
15
- OutputStage["BRIDGE_RCVD"] = "BRIDGE_RCVD";
16
- OutputStage["SWAP_USDC_GAS"] = "SWAP_USDC_GAS";
17
- OutputStage["SWAP_USDC_TRG"] = "SWAP_USDC_TRG";
18
- OutputStage["LTRL_TRG_SENT"] = "LTRL_TRG_SENT";
19
- OutputStage["LTRL_TRG_RCVD"] = "LTRL_TRG_RCVD";
20
- OutputStage["SWAP_TRG_DST"] = "SWAP_TRG_DST";
21
- OutputStage["XFER_TRG_DST"] = "XFER_TRG_DST";
22
- OutputStage["REFUND_NATIVE"] = "REFUND_NATIVE";
23
- OutputStage["FINALIZED"] = "FINALIZED";
24
- })(OutputStage || (OutputStage = {}));
25
- /**
26
- * Get human-readable status text from output stage
27
- */
28
- export function getStatusTextFromStage(stage) {
29
- switch (stage) {
30
- case OutputStage.NONE:
31
- return 'Uncertain state';
32
- case OutputStage.INIT:
33
- return 'Verifying deposit';
34
- case OutputStage.FUNDED:
35
- return 'Initializing';
36
- case OutputStage.REDEEMED:
37
- return 'Anonymizing';
38
- case OutputStage.IBC_SENT:
39
- return 'Moving';
40
- case OutputStage.IBC_RCVD:
41
- return 'Staging';
42
- case OutputStage.BRIDGE_SENT:
43
- case OutputStage.BRIDGE_CFRM:
44
- case OutputStage.BRIDGE_RCVD:
45
- return 'Bridging';
46
- case OutputStage.SWAP_USDC_GAS:
47
- return 'Fueling';
48
- case OutputStage.SWAP_USDC_TRG:
49
- return 'Swapping';
50
- case OutputStage.LTRL_TRG_SENT:
51
- case OutputStage.LTRL_TRG_RCVD:
52
- case OutputStage.SWAP_TRG_DST:
53
- return 'Finalizing';
54
- case OutputStage.XFER_TRG_DST:
55
- case OutputStage.REFUND_NATIVE:
56
- case OutputStage.FINALIZED:
57
- return 'Complete';
58
- default:
59
- return 'Processing';
60
- }
61
- }
62
- /**
63
- * Get progress value (0-1) from output stage
64
- */
65
- export function getProgressFromStage(stage) {
66
- switch (stage) {
67
- case OutputStage.NONE:
68
- return 0;
69
- case OutputStage.INIT:
70
- return 0;
71
- case OutputStage.FUNDED:
72
- return 0.1;
73
- case OutputStage.REDEEMED:
74
- return 0.15;
75
- case OutputStage.IBC_SENT:
76
- return 0.25;
77
- case OutputStage.IBC_RCVD:
78
- return 0.35;
79
- case OutputStage.BRIDGE_SENT:
80
- return 0.5;
81
- case OutputStage.BRIDGE_CFRM:
82
- return 0.6;
83
- case OutputStage.BRIDGE_RCVD:
84
- return 0.7;
85
- case OutputStage.SWAP_USDC_GAS:
86
- return 0.8;
87
- case OutputStage.SWAP_USDC_TRG:
88
- return 0.9;
89
- case OutputStage.LTRL_TRG_SENT:
90
- case OutputStage.LTRL_TRG_RCVD:
91
- case OutputStage.SWAP_TRG_DST:
92
- return 0.95;
93
- case OutputStage.XFER_TRG_DST:
94
- case OutputStage.REFUND_NATIVE:
95
- case OutputStage.FINALIZED:
96
- return 1;
97
- default:
98
- return 0;
99
- }
100
- }
101
- const wsCache = new Map();
102
- const pendingCleanups = new Map();
103
- const STATUS_UPDATE_TYPES = ['deposit', 'stage', 'transaction', 'error'];
104
- function getWsCacheKey(orderId, auth) {
105
- return `${orderId}|${auth}`;
106
- }
107
- /**
108
- * Type guard: result is a full order status snapshot (priority, outputs, optional metadata)
109
- * rather than a discriminated StatusUpdate (type + data). Server may push full status
110
- * with no `type` field.
111
- */
112
- function isFullOrderStatus(result) {
113
- if (typeof result !== 'object' || result === null)
114
- return false;
115
- const r = result;
116
- const hasOutputs = Array.isArray(r.outputs) && r.outputs.length > 0;
117
- const hasType = typeof r.type === 'string' && STATUS_UPDATE_TYPES.includes(r.type);
118
- return hasOutputs && !hasType;
119
- }
120
- /** True if status has at least one output at FINALIZED */
121
- function hasFinalizedOutput(status) {
122
- return status.outputs?.some((o) => o.stage === OutputStage.FINALIZED) ?? false;
123
- }
124
- // Build WebSocket URL from client config (pure function, no React deps)
125
- function buildWsUrl(client) {
126
- let wsUrl;
127
- if (typeof window !== 'undefined' && window.__WEBSOCKET_URL__) {
128
- wsUrl = window.__WEBSOCKET_URL__;
129
- }
130
- else if (client) {
131
- const baseUrl = client.baseUrl;
132
- try {
133
- const url = new URL(baseUrl);
134
- const protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
135
- wsUrl = `${protocol}//${url.host}/websocket`;
136
- }
137
- catch {
138
- wsUrl = baseUrl.replace(/^https?:\/\//, 'wss://').replace(/^http:\/\//, 'ws://') + '/websocket';
139
- }
140
- }
141
- else if (typeof window !== 'undefined') {
142
- wsUrl = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/websocket`;
143
- }
144
- else {
145
- wsUrl = 'ws://localhost/websocket';
146
- }
147
- // Ensure URL is exactly: wss://domain/websocket (no extra path/query)
148
- try {
149
- const url = new URL(wsUrl);
150
- const protocol = url.protocol === 'https:' || url.protocol === 'wss:' ? 'wss:' : 'ws:';
151
- wsUrl = `${protocol}//${url.host}/websocket`;
152
- }
153
- catch {
154
- wsUrl = wsUrl.split('?')[0].split('#')[0].replace(/\/+$/, '') + '/websocket';
155
- }
156
- return wsUrl;
157
- }
158
- // ─── Hook ───────────────────────────────────────────────────────────────────
159
- /**
160
- * React hook for tracking SilentSwap orders via WebSocket
161
- *
162
- * Uses a module-level singleton: one WebSocket per order, reused across
163
- * React lifecycle events (Strict Mode, re-renders, effect re-runs).
164
- * Reconnection matches Svelte ws_order_subscribe: immediate reconnect,
165
- * 5s delay if idle >90s, no exponential backoff.
2
+ * Order tracking via WebSocket state and connections live in OrdersContext.
3
+ * This hook subscribes to tracking for (orderId, auth) and returns the state from context.
166
4
  */
5
+ import { useEffect, useMemo } from 'react';
6
+ import { useOrdersContext } from '../../contexts/OrdersContext.js';
7
+ import { getOrderTrackingCacheKey, getStatusTextFromStage, getProgressFromStage, DEFAULT_ORDER_TRACKING_STATE, } from '../../contexts/orderTrackingTypes.js';
8
+ // Re-export types and helpers for consumers
9
+ export { OutputStage, getStatusTextFromStage, getProgressFromStage, getOrderTrackingCacheKey, } from '../../contexts/orderTrackingTypes.js';
167
10
  export function useOrderTracking({ client, orderId: initialOrderId, viewingAuth: initialAuth, onStatusUpdate, onError, onComplete, fetchAssetPrice, } = {}) {
168
- const [isConnected, setIsConnected] = useState(false);
169
- const [isLoading, setIsLoading] = useState(false);
170
- const [error, setError] = useState(null);
171
- const [orderStatus, setOrderStatus] = useState(null);
172
- const [deposit, setDeposit] = useState(null);
173
- const [outputs, setOutputs] = useState([]);
174
- const [progresses, setProgresses] = useState([undefined]);
175
- const [statusTexts, setStatusTexts] = useState(['Connecting']);
176
- const [completedTimestamp, setCompletedTimestamp] = useState(null);
177
- const [isComplete, setIsComplete] = useState(false);
178
- // Store callback props in refs — read from handlers without causing re-creation
179
- const onStatusUpdateRef = useRef(onStatusUpdate);
180
- const onErrorRef = useRef(onError);
181
- const onCompleteRef = useRef(onComplete);
182
- const fetchAssetPriceRef = useRef(fetchAssetPrice);
183
- const clientRef = useRef(client);
184
- onStatusUpdateRef.current = onStatusUpdate;
185
- onErrorRef.current = onError;
186
- onCompleteRef.current = onComplete;
187
- fetchAssetPriceRef.current = fetchAssetPrice;
188
- clientRef.current = client;
189
- // Per-instance refs (point to the shared cached connection)
190
- const wsRef = useRef(null);
191
- const orderIdRef = useRef(undefined);
192
- const authRef = useRef(undefined);
193
- const cachedConnRef = useRef(null);
194
- const reconnectTimeoutRef = useRef(null);
195
- /** Latest order status (for same order) so we never overwrite finalized with stale "Verifying deposit" */
196
- const orderStatusRef = useRef(null);
197
- // Update output state when status changes
198
- const updateOutput = useCallback((index, stage, timestamp = Date.now(), asset) => {
199
- const progress = getProgressFromStage(stage);
200
- const status = getStatusTextFromStage(stage);
201
- setProgresses((prev) => {
202
- const newProgresses = [...prev];
203
- newProgresses[index] = progress;
204
- return newProgresses;
205
- });
206
- setStatusTexts((prev) => {
207
- const newTexts = [...prev];
208
- newTexts[index] = status;
209
- return newTexts;
210
- });
211
- setOutputs((prev) => {
212
- const newOutputs = [...prev];
213
- newOutputs[index] = {
214
- ...newOutputs[index],
215
- index,
216
- stage,
217
- timestamp,
218
- ...(asset ? { asset } : {}),
219
- ...(prev[index]?.recipient ? { recipient: prev[index].recipient } : {}),
220
- };
221
- return newOutputs;
222
- });
223
- // Check if all outputs are complete
224
- // Use Math.min for earliest completion time (matching Svelte)
225
- setProgresses((currentProgresses) => {
226
- if (currentProgresses.every((p) => (p ?? 0) >= 1)) {
227
- setIsComplete(true);
228
- setCompletedTimestamp((prev) => prev ? Math.min(prev, timestamp) : timestamp);
229
- onCompleteRef.current?.();
230
- }
231
- return currentProgresses;
232
- });
233
- // Fetch USD price if missing (matching Svelte: k_asset?.priceUsd())
234
- if (asset && asset.priceUsd === undefined && fetchAssetPriceRef.current) {
235
- fetchAssetPriceRef.current(asset.caip19).then((price) => {
236
- if (typeof price === 'number' && price > 0) {
237
- setOutputs((prev) => {
238
- const newOutputs = [...prev];
239
- if (newOutputs[index]?.asset) {
240
- newOutputs[index] = {
241
- ...newOutputs[index],
242
- asset: { ...newOutputs[index].asset, priceUsd: price },
243
- };
244
- }
245
- return newOutputs;
246
- });
247
- }
248
- }).catch(() => { });
249
- }
250
- }, []);
251
- // Apply full OrderStatus to state (and cache). Used for initial connect response and server pushes with no `type`.
252
- // Never overwrite a more advanced state (e.g. FINALIZED) with a stale one (e.g. "Verifying deposit") when messages arrive out of order.
253
- const applyFullOrderStatusToState = useCallback((status, cacheConn) => {
254
- const current = orderStatusRef.current;
255
- const currentFinalized = current && hasFinalizedOutput(current);
256
- const incomingFinalized = hasFinalizedOutput(status);
257
- if (currentFinalized && !incomingFinalized) {
258
- return;
259
- }
260
- orderStatusRef.current = status;
261
- if (cacheConn) {
262
- cacheConn.initialStatus = status;
263
- }
264
- setOrderStatus(status);
265
- if (status.deposit) {
266
- setDeposit({
267
- amount: status.deposit.amount,
268
- timestamp: status.deposit.timestamp,
269
- duration: status.deposit.duration,
270
- orderId: status.deposit.orderId,
271
- tx: status.deposit.tx,
272
- });
273
- }
274
- const numOutputs = status.outputs?.length ?? 1;
275
- setProgresses(new Array(numOutputs).fill(undefined));
276
- setStatusTexts(new Array(numOutputs).fill('Connecting'));
277
- status.outputs?.forEach((output, index) => {
278
- const outputAsset = output.output ?? (output.asset ? {
279
- caip19: output.asset,
280
- amount: output.value || '0',
281
- decimals: 0,
282
- } : undefined);
283
- setOutputs((prev) => {
284
- const newOutputs = [...prev];
285
- newOutputs[index] = {
286
- index,
287
- stage: output.stage,
288
- timestamp: output.timestamp,
289
- recipient: output.recipient,
290
- asset: outputAsset,
291
- txs: output.txs,
292
- };
293
- return newOutputs;
294
- });
295
- updateOutput(index, output.stage, output.timestamp, outputAsset);
296
- });
297
- onStatusUpdateRef.current?.(status);
298
- }, [updateOutput]);
299
- // Handle status updates from WebSocket — also caches in the connection for future subscribers
300
- const handleStatusUpdate = useCallback((update) => {
301
- // Cache the update so new subscribers get it immediately
302
- cachedConnRef.current?.statusUpdates.push(update);
303
- switch (update.type) {
304
- case 'deposit': {
305
- setDeposit({
306
- amount: update.data.amount,
307
- timestamp: update.data.timestamp,
308
- duration: update.data.duration,
309
- orderId: update.data.orderId,
310
- tx: update.data.tx,
311
- });
312
- break;
313
- }
314
- case 'stage': {
315
- updateOutput(update.data.index, update.data.stage, update.data.timestamp, update.data.asset);
316
- break;
317
- }
318
- case 'transaction': {
319
- setOutputs((prev) => {
320
- const newOutputs = [...prev];
321
- if (newOutputs[update.data.index]) {
322
- const existingTxs = newOutputs[update.data.index].txs || {};
323
- newOutputs[update.data.index] = {
324
- ...newOutputs[update.data.index],
325
- txs: {
326
- ...existingTxs,
327
- [update.data.kind]: {
328
- txId: update.data.txId,
329
- chain: update.data.chain,
330
- },
331
- },
332
- };
333
- }
334
- return newOutputs;
335
- });
336
- break;
337
- }
338
- case 'error': {
339
- const err = new Error(update.data.message || 'Order tracking error');
340
- setError(err);
341
- onErrorRef.current?.(err);
342
- break;
343
- }
344
- }
345
- }, [updateOutput]);
346
- // Submit JSON-RPC request on the cached connection
347
- const submitRequest = useCallback((conn, method, params, handler) => {
348
- if (conn.ws.readyState !== WebSocket.OPEN) {
349
- console.warn('WebSocket not connected, cannot send request');
350
- return -1;
351
- }
352
- const requestId = conn.requestCounter++;
353
- const request = {
354
- jsonrpc: '2.0',
355
- id: requestId,
356
- method,
357
- params,
358
- };
359
- if (handler) {
360
- conn.responseHandlers.set(requestId, handler);
361
- }
362
- conn.ws.send(JSON.stringify(request));
363
- return requestId;
364
- }, []);
365
- // Connect to WebSocket — uses module-level singleton cache
366
- const connect = useCallback((orderId, auth) => {
367
- const key = getWsCacheKey(orderId, auth);
368
- // Cancel any pending cleanup for this order (React Strict Mode recovery)
369
- const pendingCleanup = pendingCleanups.get(key);
370
- if (pendingCleanup) {
371
- clearTimeout(pendingCleanup);
372
- pendingCleanups.delete(key);
373
- }
374
- // Clear any pending reconnect timeout
375
- if (reconnectTimeoutRef.current) {
376
- clearTimeout(reconnectTimeoutRef.current);
377
- reconnectTimeoutRef.current = null;
378
- }
379
- orderIdRef.current = orderId;
380
- authRef.current = auth;
381
- // ── Singleton: reuse existing active connection for this order ──
382
- const cached = wsCache.get(key);
383
- if (cached && (cached.ws.readyState === WebSocket.OPEN || cached.ws.readyState === WebSocket.CONNECTING)) {
384
- // Close any WS for a different order
385
- if (wsRef.current && wsRef.current !== cached.ws) {
386
- wsRef.current.close();
387
- }
388
- wsRef.current = cached.ws;
389
- cachedConnRef.current = cached;
390
- if (cached.ws.readyState === WebSocket.OPEN) {
391
- setIsConnected(true);
392
- setIsLoading(false);
393
- }
394
- else {
395
- setIsLoading(true);
396
- }
397
- // Hydrate React state from cached data so new subscribers skip skeletons
398
- if (cached.initialStatus) {
399
- const status = cached.initialStatus;
400
- orderStatusRef.current = status;
401
- setOrderStatus(status);
402
- if (status.deposit) {
403
- setDeposit({
404
- amount: status.deposit.amount,
405
- timestamp: status.deposit.timestamp,
406
- duration: status.deposit.duration,
407
- orderId: status.deposit.orderId,
408
- tx: status.deposit.tx,
409
- });
410
- }
411
- const numOutputs = status.outputs?.length ?? 1;
412
- setProgresses(new Array(numOutputs).fill(undefined));
413
- setStatusTexts(new Array(numOutputs).fill('Connecting'));
414
- status.outputs?.forEach((output, index) => {
415
- // Use resolved output.output if available, otherwise construct from top-level fields
416
- // At early stages (INIT), output.output is undefined but asset/value are present
417
- const outputAsset = output.output ?? (output.asset ? {
418
- caip19: output.asset,
419
- amount: output.value || '0',
420
- decimals: 0,
421
- } : undefined);
422
- setOutputs((prev) => {
423
- const newOutputs = [...prev];
424
- newOutputs[index] = {
425
- index,
426
- stage: output.stage,
427
- timestamp: output.timestamp,
428
- recipient: output.recipient,
429
- asset: outputAsset,
430
- txs: output.txs,
431
- };
432
- return newOutputs;
433
- });
434
- updateOutput(index, output.stage, output.timestamp, outputAsset);
435
- });
436
- // Replay any status updates that arrived after the initial connect
437
- for (const update of cached.statusUpdates) {
438
- handleStatusUpdate(update);
439
- }
440
- onStatusUpdateRef.current?.(status);
441
- }
442
- return;
443
- }
444
- // ── No active connection — create new WebSocket ──
445
- // Close any existing WS for a different order
446
- if (wsRef.current) {
447
- wsRef.current.close();
448
- wsRef.current = null;
449
- }
450
- // Remove stale cache entry
451
- wsCache.delete(key);
452
- // So first response for this order is not skipped (ref may still hold previous order's finalized state)
453
- orderStatusRef.current = null;
454
- setIsLoading(true);
455
- setError(null);
456
- const wsUrl = buildWsUrl(clientRef.current);
457
- try {
458
- const ws = new WebSocket(wsUrl);
459
- // Create cached connection entry
460
- const conn = {
461
- ws,
462
- orderId,
463
- auth,
464
- requestCounter: 0,
465
- responseHandlers: new Map(),
466
- lastReceived: 0,
467
- reconnectTimeout: null,
468
- initialStatus: null,
469
- statusUpdates: [],
470
- };
471
- wsCache.set(key, conn);
472
- wsRef.current = ws;
473
- cachedConnRef.current = conn;
474
- ws.onopen = () => {
475
- conn.lastReceived = Date.now();
476
- setIsConnected(true);
477
- setIsLoading(false);
478
- // Send connect JSON-RPC request (matching Svelte)
479
- const connectRequestId = submitRequest(conn, 'connect', {
480
- auth: {
481
- orderId,
482
- viewingAuth: auth,
483
- },
484
- }, (rpcError, result) => {
485
- if (rpcError) {
486
- const err = new Error(rpcError.message || 'Connection error');
487
- setError(err);
488
- setIsLoading(false);
489
- onErrorRef.current?.(err);
490
- return;
491
- }
492
- // Handle connected response — cache for future subscribers
493
- const status = result;
494
- conn.statusUpdates = [];
495
- applyFullOrderStatusToState(status, conn);
496
- // Register status handler for subsequent updates (matching Svelte pattern).
497
- // Server may push full status (priority, outputs, metadata) with no `type` — treat as OrderStatus.
498
- if (connectRequestId >= 0) {
499
- conn.responseHandlers.set(connectRequestId, (statusError, statusResult) => {
500
- if (statusError) {
501
- const err = new Error(statusError.message || 'Status error');
502
- setError(err);
503
- onErrorRef.current?.(err);
504
- return;
505
- }
506
- if (isFullOrderStatus(statusResult)) {
507
- applyFullOrderStatusToState(statusResult, conn);
508
- return;
509
- }
510
- handleStatusUpdate(statusResult);
511
- });
512
- }
513
- onStatusUpdateRef.current?.(status);
514
- });
515
- };
516
- ws.onmessage = (event) => {
517
- conn.lastReceived = Date.now();
518
- try {
519
- const response = JSON.parse(event.data);
520
- if (response.jsonrpc !== '2.0' || typeof response.id !== 'number') {
521
- return;
522
- }
523
- // Status updates reuse the same request ID — don't delete handler
524
- const handler = conn.responseHandlers.get(response.id);
525
- if (handler) {
526
- handler(response.error, response.result);
527
- }
528
- }
529
- catch (parseError) {
530
- console.warn('Failed to parse WebSocket message:', parseError);
531
- }
532
- };
533
- ws.onerror = () => {
534
- setError(new Error('WebSocket connection error'));
535
- setIsLoading(false);
536
- };
537
- // Reconnection on close (matching Svelte ws_order_subscribe):
538
- // Always reconnect if still tracking, 5s delay if idle >90s
539
- ws.onclose = () => {
540
- // Remove from cache (stale)
541
- if (wsCache.get(key)?.ws === ws) {
542
- wsCache.delete(key);
543
- }
544
- // If this WS was replaced by a new one, don't reconnect
545
- if (wsRef.current !== ws)
546
- return;
547
- setIsConnected(false);
548
- conn.responseHandlers.clear();
549
- conn.initialStatus = null;
550
- conn.statusUpdates = [];
551
- // Reconnect if still tracking this order (matching Svelte)
552
- if (orderIdRef.current === orderId && authRef.current === auth) {
553
- const idle = Date.now() - conn.lastReceived > 90_000;
554
- if (idle) {
555
- reconnectTimeoutRef.current = setTimeout(() => {
556
- if (orderIdRef.current === orderId && authRef.current === auth) {
557
- connect(orderId, auth);
558
- }
559
- }, 5000);
560
- }
561
- else {
562
- connect(orderId, auth);
563
- }
564
- }
565
- };
566
- }
567
- catch (err) {
568
- const connectError = err instanceof Error ? err : new Error('Failed to connect to order tracking');
569
- setError(connectError);
570
- setIsLoading(false);
571
- onErrorRef.current?.(connectError);
572
- }
573
- }, [submitRequest, updateOutput, handleStatusUpdate, applyFullOrderStatusToState]);
574
- // Disconnect — schedules delayed cleanup so WS survives React Strict Mode re-mount
575
- const disconnect = useCallback(() => {
576
- // Clear any pending reconnect timeout
577
- if (reconnectTimeoutRef.current) {
578
- clearTimeout(reconnectTimeoutRef.current);
579
- reconnectTimeoutRef.current = null;
580
- }
581
- const orderId = orderIdRef.current;
582
- const auth = authRef.current;
583
- // Clear tracked order so onclose won't reconnect
584
- orderIdRef.current = undefined;
585
- authRef.current = undefined;
586
- orderStatusRef.current = null;
587
- const ws = wsRef.current;
588
- wsRef.current = null;
589
- cachedConnRef.current = null;
590
- // Schedule delayed cleanup — if connect() is called quickly (Strict Mode),
591
- // it will cancel this timeout and reuse the cached WS
592
- if (ws && orderId && auth) {
593
- const key = getWsCacheKey(orderId, auth);
594
- pendingCleanups.set(key, setTimeout(() => {
595
- pendingCleanups.delete(key);
596
- // Only close if still in cache (wasn't reclaimed by a new connect)
597
- if (wsCache.get(key)?.ws === ws) {
598
- wsCache.delete(key);
599
- ws.close();
600
- }
601
- }, 500));
602
- }
603
- setIsConnected(false);
604
- }, []);
605
- // Auto-connect when orderId/auth are provided
606
- // connect and disconnect have stable identities (all deps are stable)
11
+ const ctx = useOrdersContext();
12
+ const stableState = useMemo(() => {
13
+ const key = initialOrderId && initialAuth
14
+ ? getOrderTrackingCacheKey(initialOrderId, initialAuth)
15
+ : '';
16
+ const state = key ? ctx.getOrderTrackingState(initialOrderId, initialAuth) : null;
17
+ return state ?? DEFAULT_ORDER_TRACKING_STATE;
18
+ }, [initialOrderId, initialAuth, ctx]);
19
+ const options = useMemo(() => ({
20
+ client,
21
+ onStatusUpdate,
22
+ onError,
23
+ onComplete,
24
+ fetchAssetPrice,
25
+ }), [client, onStatusUpdate, onError, onComplete, fetchAssetPrice]);
26
+ const { connectOrderTracking, disconnectOrderTracking } = ctx;
607
27
  useEffect(() => {
608
- if (initialOrderId && initialAuth && clientRef.current) {
609
- connect(initialOrderId, initialAuth);
610
- }
28
+ if (!initialOrderId || !initialAuth || !client)
29
+ return;
30
+ connectOrderTracking(initialOrderId, initialAuth, options);
611
31
  return () => {
612
- disconnect();
32
+ disconnectOrderTracking(initialOrderId, initialAuth);
613
33
  };
614
- }, [initialOrderId, initialAuth, connect, disconnect]);
34
+ }, [initialOrderId, initialAuth, client, options, connectOrderTracking, disconnectOrderTracking]);
615
35
  return {
616
- // State
617
- isConnected,
618
- isLoading,
619
- error,
620
- orderStatus,
621
- deposit,
622
- outputs,
623
- progresses,
624
- statusTexts,
625
- completedTimestamp,
626
- isComplete,
627
- // Methods
628
- connect,
629
- disconnect,
36
+ ...stableState,
37
+ connect: (orderId, auth) => {
38
+ ctx.connectOrderTracking(orderId, auth, options);
39
+ },
40
+ disconnect: () => {
41
+ if (initialOrderId && initialAuth) {
42
+ ctx.disconnectOrderTracking(initialOrderId, initialAuth);
43
+ }
44
+ },
630
45
  getStatusText: getStatusTextFromStage,
631
46
  getProgress: getProgressFromStage,
632
47
  };