@oxyhq/services 5.16.25 → 5.16.27
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.
- package/lib/commonjs/ui/context/OxyContext.js +160 -37
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/useTransferQueries.js +137 -0
- package/lib/commonjs/ui/hooks/useTransferQueries.js.map +1 -0
- package/lib/commonjs/ui/stores/transferStore.js +161 -0
- package/lib/commonjs/ui/stores/transferStore.js.map +1 -0
- package/lib/module/ui/context/OxyContext.js +160 -37
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/useTransferQueries.js +132 -0
- package/lib/module/ui/hooks/useTransferQueries.js.map +1 -0
- package/lib/module/ui/stores/transferStore.js +155 -0
- package/lib/module/ui/stores/transferStore.js.map +1 -0
- package/lib/typescript/ui/context/OxyContext.d.ts +15 -2
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/useTransferQueries.d.ts +36 -0
- package/lib/typescript/ui/hooks/useTransferQueries.d.ts.map +1 -0
- package/lib/typescript/ui/stores/transferStore.d.ts +39 -0
- package/lib/typescript/ui/stores/transferStore.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/ui/context/OxyContext.tsx +178 -37
- package/src/ui/hooks/useTransferQueries.ts +154 -0
- package/src/ui/stores/transferStore.ts +201 -0
|
@@ -34,6 +34,8 @@ import { translate } from '../../i18n';
|
|
|
34
34
|
import { updateAvatarVisibility, updateProfileWithAvatar } from '../utils/avatarUtils';
|
|
35
35
|
import { useAccountStore } from '../stores/accountStore';
|
|
36
36
|
import { logger as loggerUtil } from '../../utils/loggerUtils';
|
|
37
|
+
import { useTransferStore, useTransferCodesForPersistence } from '../stores/transferStore';
|
|
38
|
+
import { useCheckPendingTransfers } from '../hooks/useTransferQueries';
|
|
37
39
|
|
|
38
40
|
export interface OxyContextState {
|
|
39
41
|
user: User | null;
|
|
@@ -59,9 +61,12 @@ export interface OxyContextState {
|
|
|
59
61
|
isIdentitySynced: () => Promise<boolean>;
|
|
60
62
|
syncIdentity: () => Promise<User>;
|
|
61
63
|
deleteIdentityAndClearAccount: (skipBackup?: boolean, force?: boolean, userConfirmed?: boolean) => Promise<void>;
|
|
62
|
-
storeTransferCode: (transferId: string, code: string, sourceDeviceId: string | null, publicKey: string) => void
|
|
63
|
-
getTransferCode: (transferId: string) => { code: string; sourceDeviceId: string | null; publicKey: string; timestamp: number } | null;
|
|
64
|
-
clearTransferCode: (transferId: string) => void
|
|
64
|
+
storeTransferCode: (transferId: string, code: string, sourceDeviceId: string | null, publicKey: string) => Promise<void>;
|
|
65
|
+
getTransferCode: (transferId: string) => { code: string; sourceDeviceId: string | null; publicKey: string; timestamp: number; state: 'pending' | 'completed' | 'failed' } | null;
|
|
66
|
+
clearTransferCode: (transferId: string) => Promise<void>;
|
|
67
|
+
getAllPendingTransfers: () => Array<{ transferId: string; data: { code: string; sourceDeviceId: string | null; publicKey: string; timestamp: number; state: 'pending' | 'completed' | 'failed' } }>;
|
|
68
|
+
getActiveTransferId: () => string | null;
|
|
69
|
+
updateTransferState: (transferId: string, state: 'pending' | 'completed' | 'failed') => Promise<void>;
|
|
65
70
|
|
|
66
71
|
// Identity sync state (reactive, from Zustand store)
|
|
67
72
|
identitySyncState: {
|
|
@@ -384,6 +389,23 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
384
389
|
oxyServices.clearCache();
|
|
385
390
|
}, [queryClient, storage, clearSessionState, logger, oxyServices]);
|
|
386
391
|
|
|
392
|
+
// Extract Zustand store functions early (before they're used in callbacks)
|
|
393
|
+
const getAllPendingTransfersStore = useTransferStore((state) => state.getAllPendingTransfers);
|
|
394
|
+
const getActiveTransferIdStore = useTransferStore((state) => state.getActiveTransferId);
|
|
395
|
+
const storeTransferCodeStore = useTransferStore((state) => state.storeTransferCode);
|
|
396
|
+
const getTransferCodeStore = useTransferStore((state) => state.getTransferCode);
|
|
397
|
+
const updateTransferStateStore = useTransferStore((state) => state.updateTransferState);
|
|
398
|
+
const clearTransferCodeStore = useTransferStore((state) => state.clearTransferCode);
|
|
399
|
+
|
|
400
|
+
// Transfer code management functions (must be defined before deleteIdentityAndClearAccount)
|
|
401
|
+
const getAllPendingTransfers = useCallback(() => {
|
|
402
|
+
return getAllPendingTransfersStore();
|
|
403
|
+
}, [getAllPendingTransfersStore]);
|
|
404
|
+
|
|
405
|
+
const getActiveTransferId = useCallback(() => {
|
|
406
|
+
return getActiveTransferIdStore();
|
|
407
|
+
}, [getActiveTransferIdStore]);
|
|
408
|
+
|
|
387
409
|
// Delete identity and clear all account data
|
|
388
410
|
// In accounts app, deleting identity means losing the account completely
|
|
389
411
|
const deleteIdentityAndClearAccount = useCallback(async (
|
|
@@ -391,12 +413,30 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
391
413
|
force: boolean = false,
|
|
392
414
|
userConfirmed: boolean = false
|
|
393
415
|
): Promise<void> => {
|
|
416
|
+
// CRITICAL: Check for active transfers before deletion (unless force is true)
|
|
417
|
+
// This prevents accidental identity loss during transfer
|
|
418
|
+
if (!force) {
|
|
419
|
+
const pendingTransfers = getAllPendingTransfers();
|
|
420
|
+
if (pendingTransfers.length > 0) {
|
|
421
|
+
const activeTransferId = getActiveTransferId();
|
|
422
|
+
const hasActiveTransfer = activeTransferId && pendingTransfers.some((t: { transferId: string; data: any }) => t.transferId === activeTransferId);
|
|
423
|
+
|
|
424
|
+
if (hasActiveTransfer) {
|
|
425
|
+
throw new Error(
|
|
426
|
+
'Cannot delete identity: An active identity transfer is in progress. ' +
|
|
427
|
+
'Please wait for the transfer to complete or cancel it first. ' +
|
|
428
|
+
'If you proceed, you may lose access to your identity permanently.'
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
394
434
|
// First, clear all account data
|
|
395
435
|
await clearAllAccountData();
|
|
396
436
|
|
|
397
437
|
// Then delete the identity keys
|
|
398
438
|
await KeyManager.deleteIdentity(skipBackup, force, userConfirmed);
|
|
399
|
-
}, [clearAllAccountData]);
|
|
439
|
+
}, [clearAllAccountData, getAllPendingTransfers, getActiveTransferId]);
|
|
400
440
|
|
|
401
441
|
// Network reconnect sync - TanStack Query automatically retries mutations on reconnect
|
|
402
442
|
// We only need to sync identity if it's not synced
|
|
@@ -474,6 +514,10 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
474
514
|
loggerUtil.debug('Identity sync timeout (expected when offline)', { component: 'OxyContext', method: 'checkNetworkAndSync' }, syncError as unknown);
|
|
475
515
|
}
|
|
476
516
|
}
|
|
517
|
+
|
|
518
|
+
// Check for pending transfers that may have completed while offline
|
|
519
|
+
// This is handled by useCheckPendingTransfers hook which runs automatically
|
|
520
|
+
// when authenticated and online
|
|
477
521
|
}
|
|
478
522
|
|
|
479
523
|
// TanStack Query will automatically retry pending mutations
|
|
@@ -515,7 +559,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
515
559
|
clearTimeout(checkTimeout);
|
|
516
560
|
}
|
|
517
561
|
};
|
|
518
|
-
}, [oxyServices, storage, syncIdentity, logger]);
|
|
562
|
+
}, [oxyServices, storage, syncIdentity, logger, hasIdentity, isAuthenticated]);
|
|
519
563
|
|
|
520
564
|
const { getDeviceSessions, logoutAllDeviceSessions, updateDeviceName } = useDeviceManagement({
|
|
521
565
|
oxyServices,
|
|
@@ -634,51 +678,110 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
634
678
|
// The JWT token's userId field contains the MongoDB ObjectId
|
|
635
679
|
const userId = oxyServices.getCurrentUserId() || user?.id;
|
|
636
680
|
|
|
637
|
-
//
|
|
638
|
-
|
|
639
|
-
const
|
|
681
|
+
// Use Zustand store for transfer state management
|
|
682
|
+
const TRANSFER_CODES_STORAGE_KEY = `${storageKeyPrefix}_transfer_codes`;
|
|
683
|
+
const ACTIVE_TRANSFER_STORAGE_KEY = `${storageKeyPrefix}_active_transfer_id`;
|
|
684
|
+
const isRestored = useTransferStore((state) => state.isRestored);
|
|
685
|
+
const restoreFromStorage = useTransferStore((state) => state.restoreFromStorage);
|
|
686
|
+
const markRestored = useTransferStore((state) => state.markRestored);
|
|
687
|
+
const cleanupExpired = useTransferStore((state) => state.cleanupExpired);
|
|
640
688
|
|
|
641
|
-
//
|
|
689
|
+
// Load transfer codes from storage on startup (only once)
|
|
642
690
|
useEffect(() => {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
691
|
+
if (!storage || !isStorageReady || isRestored) return;
|
|
692
|
+
|
|
693
|
+
const loadTransferCodes = async () => {
|
|
694
|
+
try {
|
|
695
|
+
// Load transfer codes
|
|
696
|
+
const storedCodes = await storage.getItem(TRANSFER_CODES_STORAGE_KEY);
|
|
697
|
+
const storedActiveTransferId = await storage.getItem(ACTIVE_TRANSFER_STORAGE_KEY);
|
|
698
|
+
|
|
699
|
+
const parsedCodes = storedCodes ? JSON.parse(storedCodes) : {};
|
|
700
|
+
const activeTransferId = storedActiveTransferId || null;
|
|
701
|
+
|
|
702
|
+
// Restore to Zustand store (store handles validation and expiration)
|
|
703
|
+
restoreFromStorage(parsedCodes, activeTransferId);
|
|
704
|
+
markRestored();
|
|
705
|
+
|
|
706
|
+
if (__DEV__ && Object.keys(parsedCodes).length > 0) {
|
|
707
|
+
logger('Restored transfer codes from storage', {
|
|
708
|
+
count: Object.keys(parsedCodes).length,
|
|
709
|
+
hasActiveTransfer: !!activeTransferId,
|
|
710
|
+
});
|
|
652
711
|
}
|
|
653
|
-
})
|
|
712
|
+
} catch (error) {
|
|
713
|
+
if (__DEV__) {
|
|
714
|
+
logger('Failed to load transfer codes from storage', error);
|
|
715
|
+
}
|
|
716
|
+
// Mark as restored even on error to prevent retries
|
|
717
|
+
markRestored();
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
loadTransferCodes();
|
|
722
|
+
}, [storage, isStorageReady, isRestored, restoreFromStorage, markRestored, logger, storageKeyPrefix]);
|
|
723
|
+
|
|
724
|
+
// Persist transfer codes to storage whenever store changes
|
|
725
|
+
const { transferCodes, activeTransferId } = useTransferCodesForPersistence();
|
|
726
|
+
useEffect(() => {
|
|
727
|
+
if (!storage || !isStorageReady || !isRestored) return;
|
|
728
|
+
|
|
729
|
+
const persistTransferCodes = async () => {
|
|
730
|
+
try {
|
|
731
|
+
await storage.setItem(TRANSFER_CODES_STORAGE_KEY, JSON.stringify(transferCodes));
|
|
732
|
+
|
|
733
|
+
if (activeTransferId) {
|
|
734
|
+
await storage.setItem(ACTIVE_TRANSFER_STORAGE_KEY, activeTransferId);
|
|
735
|
+
} else {
|
|
736
|
+
await storage.removeItem(ACTIVE_TRANSFER_STORAGE_KEY);
|
|
737
|
+
}
|
|
738
|
+
} catch (error) {
|
|
739
|
+
if (__DEV__) {
|
|
740
|
+
logger('Failed to persist transfer codes', error);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
persistTransferCodes();
|
|
746
|
+
}, [transferCodes, activeTransferId, storage, isStorageReady, isRestored, logger]);
|
|
747
|
+
|
|
748
|
+
// Cleanup expired transfer codes (every minute)
|
|
749
|
+
useEffect(() => {
|
|
750
|
+
const cleanup = setInterval(() => {
|
|
751
|
+
cleanupExpired();
|
|
654
752
|
}, 60000); // Check every minute
|
|
655
753
|
|
|
656
754
|
return () => clearInterval(cleanup);
|
|
657
|
-
}, [
|
|
658
|
-
|
|
659
|
-
// Transfer code management functions
|
|
660
|
-
const storeTransferCode = useCallback((transferId: string, code: string, sourceDeviceId: string | null, publicKey: string) => {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
sourceDeviceId,
|
|
664
|
-
publicKey,
|
|
665
|
-
timestamp: Date.now(),
|
|
666
|
-
});
|
|
755
|
+
}, [cleanupExpired]);
|
|
756
|
+
|
|
757
|
+
// Transfer code management functions using Zustand store
|
|
758
|
+
const storeTransferCode = useCallback(async (transferId: string, code: string, sourceDeviceId: string | null, publicKey: string) => {
|
|
759
|
+
storeTransferCodeStore(transferId, code, sourceDeviceId, publicKey);
|
|
760
|
+
|
|
667
761
|
if (__DEV__) {
|
|
668
762
|
logger('Stored transfer code', { transferId, sourceDeviceId, publicKey: publicKey.substring(0, 16) + '...' });
|
|
669
763
|
}
|
|
670
|
-
}, [logger]);
|
|
764
|
+
}, [logger, storeTransferCodeStore]);
|
|
671
765
|
|
|
672
766
|
const getTransferCode = useCallback((transferId: string) => {
|
|
673
|
-
return
|
|
674
|
-
}, []);
|
|
767
|
+
return getTransferCodeStore(transferId);
|
|
768
|
+
}, [getTransferCodeStore]);
|
|
769
|
+
|
|
770
|
+
const updateTransferState = useCallback(async (transferId: string, state: 'pending' | 'completed' | 'failed') => {
|
|
771
|
+
updateTransferStateStore(transferId, state);
|
|
772
|
+
|
|
773
|
+
if (__DEV__) {
|
|
774
|
+
logger('Updated transfer state', { transferId, state });
|
|
775
|
+
}
|
|
776
|
+
}, [logger, updateTransferStateStore]);
|
|
675
777
|
|
|
676
|
-
const clearTransferCode = useCallback((transferId: string) => {
|
|
677
|
-
|
|
778
|
+
const clearTransferCode = useCallback(async (transferId: string) => {
|
|
779
|
+
clearTransferCodeStore(transferId);
|
|
780
|
+
|
|
678
781
|
if (__DEV__) {
|
|
679
782
|
logger('Cleared transfer code', { transferId });
|
|
680
783
|
}
|
|
681
|
-
}, [logger]);
|
|
784
|
+
}, [logger, clearTransferCodeStore]);
|
|
682
785
|
|
|
683
786
|
const refreshSessionsWithUser = useCallback(
|
|
684
787
|
() => refreshSessions(userId),
|
|
@@ -697,6 +800,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
697
800
|
logout().catch((remoteError) => logger('Failed to process remote sign out', remoteError));
|
|
698
801
|
}, [logger, logout]);
|
|
699
802
|
|
|
803
|
+
// Check pending transfers when authenticated and online using TanStack Query
|
|
804
|
+
const { data: pendingTransferResults } = useCheckPendingTransfers();
|
|
805
|
+
|
|
700
806
|
const handleIdentityTransferComplete = useCallback(
|
|
701
807
|
async (data: { transferId: string; sourceDeviceId: string; publicKey: string; transferCode?: string; completedAt: string }) => {
|
|
702
808
|
try {
|
|
@@ -789,6 +895,11 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
789
895
|
return;
|
|
790
896
|
}
|
|
791
897
|
|
|
898
|
+
// NOTE: Target device verification already happened server-side when notifyTransferComplete was called
|
|
899
|
+
// The server verified that the target device is authenticated and has the matching public key
|
|
900
|
+
// Additional client-side verification is not necessary and would require source device authentication
|
|
901
|
+
// which may not be available. The existing checks (public key match, transfer code, device ID) are sufficient.
|
|
902
|
+
|
|
792
903
|
logger('All transfer verifications passed, deleting identity from source device', {
|
|
793
904
|
transferId: data.transferId,
|
|
794
905
|
sourceDeviceId: data.sourceDeviceId,
|
|
@@ -796,8 +907,31 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
796
907
|
});
|
|
797
908
|
|
|
798
909
|
try {
|
|
910
|
+
// Verify identity still exists before deletion (safety check)
|
|
911
|
+
const identityStillExists = await KeyManager.hasIdentity();
|
|
912
|
+
if (!identityStillExists) {
|
|
913
|
+
logger('Identity already deleted - skipping deletion', {
|
|
914
|
+
transferId: data.transferId,
|
|
915
|
+
});
|
|
916
|
+
await updateTransferState(data.transferId, 'completed');
|
|
917
|
+
await clearTransferCode(data.transferId);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
|
|
799
921
|
await deleteIdentityAndClearAccount(false, false, true);
|
|
800
|
-
|
|
922
|
+
|
|
923
|
+
// Verify identity was actually deleted
|
|
924
|
+
const identityDeleted = !(await KeyManager.hasIdentity());
|
|
925
|
+
if (!identityDeleted) {
|
|
926
|
+
logger('Identity deletion failed - identity still exists', {
|
|
927
|
+
transferId: data.transferId,
|
|
928
|
+
});
|
|
929
|
+
await updateTransferState(data.transferId, 'failed');
|
|
930
|
+
throw new Error('Identity deletion failed - identity still exists');
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
await updateTransferState(data.transferId, 'completed');
|
|
934
|
+
await clearTransferCode(data.transferId);
|
|
801
935
|
|
|
802
936
|
logger('Identity successfully deleted and transfer code cleared', {
|
|
803
937
|
transferId: data.transferId,
|
|
@@ -806,6 +940,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
806
940
|
toast.success('Identity successfully transferred and removed from this device');
|
|
807
941
|
} catch (deleteError: any) {
|
|
808
942
|
logger('Error during identity deletion', deleteError);
|
|
943
|
+
await updateTransferState(data.transferId, 'failed');
|
|
809
944
|
throw deleteError;
|
|
810
945
|
}
|
|
811
946
|
} catch (error: any) {
|
|
@@ -813,7 +948,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
813
948
|
toast.error(error?.message || 'Failed to remove identity from this device. Please try again manually from Security Settings.');
|
|
814
949
|
}
|
|
815
950
|
},
|
|
816
|
-
[deleteIdentityAndClearAccount, logger, getTransferCode, clearTransferCode, currentDeviceId, activeSessionId],
|
|
951
|
+
[deleteIdentityAndClearAccount, logger, getTransferCode, clearTransferCode, updateTransferState, currentDeviceId, activeSessionId, oxyServices],
|
|
817
952
|
);
|
|
818
953
|
|
|
819
954
|
useSessionSocket({
|
|
@@ -907,6 +1042,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
907
1042
|
storeTransferCode,
|
|
908
1043
|
getTransferCode,
|
|
909
1044
|
clearTransferCode,
|
|
1045
|
+
getAllPendingTransfers,
|
|
1046
|
+
getActiveTransferId,
|
|
1047
|
+
updateTransferState,
|
|
910
1048
|
identitySyncState: {
|
|
911
1049
|
isSynced: isIdentitySyncedStore ?? true,
|
|
912
1050
|
isSyncing: isSyncing ?? false,
|
|
@@ -940,6 +1078,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
940
1078
|
storeTransferCode,
|
|
941
1079
|
getTransferCode,
|
|
942
1080
|
clearTransferCode,
|
|
1081
|
+
getAllPendingTransfers,
|
|
1082
|
+
getActiveTransferId,
|
|
1083
|
+
updateTransferState,
|
|
943
1084
|
isIdentitySyncedStore,
|
|
944
1085
|
isSyncing,
|
|
945
1086
|
currentLanguage,
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { useOxy } from '../context/OxyContext';
|
|
3
|
+
import { useTransferStore } from '../stores/transferStore';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Query keys for transfer-related queries
|
|
7
|
+
*/
|
|
8
|
+
export const transferQueryKeys = {
|
|
9
|
+
all: ['transfers'] as const,
|
|
10
|
+
completion: (transferId: string) => ['transfers', 'completion', transferId] as const,
|
|
11
|
+
pending: () => ['transfers', 'pending'] as const,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Hook to check if a transfer was completed
|
|
16
|
+
* Only runs when authenticated and transferId is provided
|
|
17
|
+
*/
|
|
18
|
+
export const useCheckTransferCompletion = (transferId: string | null, enabled: boolean = true) => {
|
|
19
|
+
const { oxyServices, isAuthenticated } = useOxy();
|
|
20
|
+
|
|
21
|
+
return useQuery({
|
|
22
|
+
queryKey: transferId ? transferQueryKeys.completion(transferId) : ['transfers', 'completion', 'null'],
|
|
23
|
+
queryFn: async () => {
|
|
24
|
+
if (!transferId || !oxyServices) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const response = await oxyServices.makeRequest<{
|
|
30
|
+
completed: boolean;
|
|
31
|
+
transferId?: string;
|
|
32
|
+
sourceDeviceId?: string;
|
|
33
|
+
publicKey?: string;
|
|
34
|
+
transferCode?: string;
|
|
35
|
+
completedAt?: string;
|
|
36
|
+
}>(
|
|
37
|
+
'GET',
|
|
38
|
+
`/api/identity/check-transfer/${transferId}`,
|
|
39
|
+
undefined,
|
|
40
|
+
{ cache: false }
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return response;
|
|
44
|
+
} catch (error: any) {
|
|
45
|
+
// Handle 401 errors gracefully - don't throw, just return null
|
|
46
|
+
if (error?.status === 401 || error?.message?.includes('401') || error?.message?.includes('authentication')) {
|
|
47
|
+
if (__DEV__) {
|
|
48
|
+
console.warn('[useCheckTransferCompletion] Authentication required, skipping check');
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
enabled: enabled && !!transferId && isAuthenticated && !!oxyServices,
|
|
56
|
+
staleTime: 30 * 1000, // 30 seconds - completion status doesn't change frequently
|
|
57
|
+
gcTime: 5 * 60 * 1000, // 5 minutes
|
|
58
|
+
retry: (failureCount, error: any) => {
|
|
59
|
+
// Don't retry on 401 errors
|
|
60
|
+
if (error?.status === 401 || error?.message?.includes('401')) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return failureCount < 2;
|
|
64
|
+
},
|
|
65
|
+
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 5000),
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Hook to check all pending transfers for completion
|
|
71
|
+
* Used when app comes back online
|
|
72
|
+
*/
|
|
73
|
+
export const useCheckPendingTransfers = () => {
|
|
74
|
+
const { oxyServices, isAuthenticated } = useOxy();
|
|
75
|
+
const getAllPendingTransfers = useTransferStore((state) => state.getAllPendingTransfers);
|
|
76
|
+
const pendingTransfers = getAllPendingTransfers();
|
|
77
|
+
|
|
78
|
+
return useQuery({
|
|
79
|
+
queryKey: [...transferQueryKeys.pending(), pendingTransfers.map(t => t.transferId).join(',')],
|
|
80
|
+
queryFn: async () => {
|
|
81
|
+
if (!oxyServices || pendingTransfers.length === 0) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const results: Array<{
|
|
86
|
+
transferId: string;
|
|
87
|
+
completed: boolean;
|
|
88
|
+
data?: {
|
|
89
|
+
transferId?: string;
|
|
90
|
+
sourceDeviceId?: string;
|
|
91
|
+
publicKey?: string;
|
|
92
|
+
transferCode?: string;
|
|
93
|
+
completedAt?: string;
|
|
94
|
+
};
|
|
95
|
+
}> = [];
|
|
96
|
+
|
|
97
|
+
// Check each pending transfer
|
|
98
|
+
for (const { transferId, data } of pendingTransfers) {
|
|
99
|
+
try {
|
|
100
|
+
const response = await oxyServices.makeRequest<{
|
|
101
|
+
completed: boolean;
|
|
102
|
+
transferId?: string;
|
|
103
|
+
sourceDeviceId?: string;
|
|
104
|
+
publicKey?: string;
|
|
105
|
+
transferCode?: string;
|
|
106
|
+
completedAt?: string;
|
|
107
|
+
}>(
|
|
108
|
+
'GET',
|
|
109
|
+
`/api/identity/check-transfer/${transferId}`,
|
|
110
|
+
undefined,
|
|
111
|
+
{ cache: false }
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (response.completed && response.publicKey === data.publicKey) {
|
|
115
|
+
results.push({
|
|
116
|
+
transferId,
|
|
117
|
+
completed: true,
|
|
118
|
+
data: response,
|
|
119
|
+
});
|
|
120
|
+
} else {
|
|
121
|
+
results.push({
|
|
122
|
+
transferId,
|
|
123
|
+
completed: false,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
} catch (error: any) {
|
|
127
|
+
// Handle 401 errors gracefully - skip this transfer
|
|
128
|
+
if (error?.status === 401 || error?.message?.includes('401') || error?.message?.includes('authentication')) {
|
|
129
|
+
if (__DEV__) {
|
|
130
|
+
console.warn(`[useCheckPendingTransfers] Authentication required for transfer ${transferId}, skipping`);
|
|
131
|
+
}
|
|
132
|
+
results.push({
|
|
133
|
+
transferId,
|
|
134
|
+
completed: false,
|
|
135
|
+
});
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// For other errors, mark as not completed
|
|
139
|
+
results.push({
|
|
140
|
+
transferId,
|
|
141
|
+
completed: false,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return results;
|
|
147
|
+
},
|
|
148
|
+
enabled: isAuthenticated && !!oxyServices && pendingTransfers.length > 0,
|
|
149
|
+
staleTime: 30 * 1000, // 30 seconds
|
|
150
|
+
gcTime: 5 * 60 * 1000, // 5 minutes
|
|
151
|
+
retry: false, // Don't retry - we'll check again on next reconnect
|
|
152
|
+
});
|
|
153
|
+
};
|
|
154
|
+
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { useShallow } from 'zustand/react/shallow';
|
|
3
|
+
|
|
4
|
+
export interface TransferCodeData {
|
|
5
|
+
code: string;
|
|
6
|
+
sourceDeviceId: string | null;
|
|
7
|
+
publicKey: string;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
state: 'pending' | 'completed' | 'failed';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TransferState {
|
|
13
|
+
// Transfer codes map: transferId -> TransferCodeData
|
|
14
|
+
transferCodes: Record<string, TransferCodeData>;
|
|
15
|
+
|
|
16
|
+
// Active transfer ID (only one active transfer at a time)
|
|
17
|
+
activeTransferId: string | null;
|
|
18
|
+
|
|
19
|
+
// Restoration flag to prevent duplicate restorations
|
|
20
|
+
isRestored: boolean;
|
|
21
|
+
|
|
22
|
+
// Actions
|
|
23
|
+
storeTransferCode: (transferId: string, code: string, sourceDeviceId: string | null, publicKey: string) => void;
|
|
24
|
+
getTransferCode: (transferId: string) => TransferCodeData | null;
|
|
25
|
+
clearTransferCode: (transferId: string) => void;
|
|
26
|
+
updateTransferState: (transferId: string, state: 'pending' | 'completed' | 'failed') => void;
|
|
27
|
+
getAllPendingTransfers: () => Array<{ transferId: string; data: TransferCodeData }>;
|
|
28
|
+
getActiveTransferId: () => string | null;
|
|
29
|
+
setActiveTransferId: (transferId: string | null) => void;
|
|
30
|
+
restoreFromStorage: (codes: Record<string, TransferCodeData>, activeTransferId: string | null) => void;
|
|
31
|
+
markRestored: () => void;
|
|
32
|
+
cleanupExpired: () => void;
|
|
33
|
+
reset: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const FIFTEEN_MINUTES = 15 * 60 * 1000;
|
|
37
|
+
|
|
38
|
+
const initialState = {
|
|
39
|
+
transferCodes: {} as Record<string, TransferCodeData>,
|
|
40
|
+
activeTransferId: null as string | null,
|
|
41
|
+
isRestored: false,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const useTransferStore = create<TransferState>((set, get) => ({
|
|
45
|
+
...initialState,
|
|
46
|
+
|
|
47
|
+
storeTransferCode: (transferId: string, code: string, sourceDeviceId: string | null, publicKey: string) => {
|
|
48
|
+
set((state) => ({
|
|
49
|
+
transferCodes: {
|
|
50
|
+
...state.transferCodes,
|
|
51
|
+
[transferId]: {
|
|
52
|
+
code,
|
|
53
|
+
sourceDeviceId,
|
|
54
|
+
publicKey,
|
|
55
|
+
timestamp: Date.now(),
|
|
56
|
+
state: 'pending',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
activeTransferId: transferId,
|
|
60
|
+
}));
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
getTransferCode: (transferId: string) => {
|
|
64
|
+
const state = get();
|
|
65
|
+
return state.transferCodes[transferId] || null;
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
clearTransferCode: (transferId: string) => {
|
|
69
|
+
set((state) => {
|
|
70
|
+
const { [transferId]: removed, ...rest } = state.transferCodes;
|
|
71
|
+
const newActiveTransferId = state.activeTransferId === transferId ? null : state.activeTransferId;
|
|
72
|
+
return {
|
|
73
|
+
transferCodes: rest,
|
|
74
|
+
activeTransferId: newActiveTransferId,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
updateTransferState: (transferId: string, newState: 'pending' | 'completed' | 'failed') => {
|
|
80
|
+
set((state) => {
|
|
81
|
+
const existing = state.transferCodes[transferId];
|
|
82
|
+
if (!existing) {
|
|
83
|
+
return state;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const updated = {
|
|
87
|
+
...existing,
|
|
88
|
+
state: newState,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Clear active transfer if completed or failed
|
|
92
|
+
const newActiveTransferId =
|
|
93
|
+
(newState === 'completed' || newState === 'failed') && state.activeTransferId === transferId
|
|
94
|
+
? null
|
|
95
|
+
: state.activeTransferId;
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
transferCodes: {
|
|
99
|
+
...state.transferCodes,
|
|
100
|
+
[transferId]: updated,
|
|
101
|
+
},
|
|
102
|
+
activeTransferId: newActiveTransferId,
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
getAllPendingTransfers: () => {
|
|
108
|
+
const state = get();
|
|
109
|
+
const pending: Array<{ transferId: string; data: TransferCodeData }> = [];
|
|
110
|
+
|
|
111
|
+
Object.entries(state.transferCodes).forEach(([transferId, data]) => {
|
|
112
|
+
if (data.state === 'pending') {
|
|
113
|
+
pending.push({ transferId, data });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return pending;
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
getActiveTransferId: () => {
|
|
121
|
+
return get().activeTransferId;
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
setActiveTransferId: (transferId: string | null) => {
|
|
125
|
+
set({ activeTransferId: transferId });
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
restoreFromStorage: (codes: Record<string, TransferCodeData>, activeTransferId: string | null) => {
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
const validCodes: Record<string, TransferCodeData> = {};
|
|
131
|
+
|
|
132
|
+
// Only restore non-expired pending transfers
|
|
133
|
+
Object.entries(codes).forEach(([transferId, data]) => {
|
|
134
|
+
if (data.state === 'pending' && (now - data.timestamp) < FIFTEEN_MINUTES) {
|
|
135
|
+
validCodes[transferId] = data;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Verify active transfer is still valid
|
|
140
|
+
let validActiveTransferId = activeTransferId;
|
|
141
|
+
if (activeTransferId && (!validCodes[activeTransferId] || validCodes[activeTransferId].state !== 'pending')) {
|
|
142
|
+
validActiveTransferId = null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
set({
|
|
146
|
+
transferCodes: validCodes,
|
|
147
|
+
activeTransferId: validActiveTransferId,
|
|
148
|
+
isRestored: true,
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
markRestored: () => {
|
|
153
|
+
set({ isRestored: true });
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
cleanupExpired: () => {
|
|
157
|
+
const now = Date.now();
|
|
158
|
+
set((state) => {
|
|
159
|
+
const validCodes: Record<string, TransferCodeData> = {};
|
|
160
|
+
let newActiveTransferId = state.activeTransferId;
|
|
161
|
+
|
|
162
|
+
Object.entries(state.transferCodes).forEach(([transferId, data]) => {
|
|
163
|
+
const age = now - data.timestamp;
|
|
164
|
+
if (age < FIFTEEN_MINUTES) {
|
|
165
|
+
validCodes[transferId] = data;
|
|
166
|
+
} else if (transferId === state.activeTransferId) {
|
|
167
|
+
newActiveTransferId = null;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
transferCodes: validCodes,
|
|
173
|
+
activeTransferId: newActiveTransferId,
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
reset: () => {
|
|
179
|
+
set(initialState);
|
|
180
|
+
},
|
|
181
|
+
}));
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Hook to get transfer codes for persistence
|
|
185
|
+
*/
|
|
186
|
+
export const useTransferCodesForPersistence = () => {
|
|
187
|
+
return useTransferStore(
|
|
188
|
+
useShallow((state) => ({
|
|
189
|
+
transferCodes: state.transferCodes,
|
|
190
|
+
activeTransferId: state.activeTransferId,
|
|
191
|
+
}))
|
|
192
|
+
);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Hook to check if store has been restored
|
|
197
|
+
*/
|
|
198
|
+
export const useTransferStoreRestored = () => {
|
|
199
|
+
return useTransferStore((state) => state.isRestored);
|
|
200
|
+
};
|
|
201
|
+
|