@oxyhq/services 5.16.24 → 5.16.26
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/core/mixins/OxyServices.user.js +14 -4
- package/lib/commonjs/core/mixins/OxyServices.user.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +280 -84
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +7 -6
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +4 -3
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/commonjs/ui/hooks/useSessionSocket.js +349 -328
- package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
- package/lib/commonjs/ui/screens/PrivacySettingsScreen.js +13 -6
- package/lib/commonjs/ui/screens/PrivacySettingsScreen.js.map +1 -1
- package/lib/module/core/mixins/OxyServices.user.js +14 -4
- package/lib/module/core/mixins/OxyServices.user.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +280 -84
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/mutations/useAccountMutations.js +7 -6
- package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
- package/lib/module/ui/hooks/queries/useAccountQueries.js +4 -3
- package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -1
- package/lib/module/ui/hooks/useSessionSocket.js +349 -328
- package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
- package/lib/module/ui/screens/PrivacySettingsScreen.js +13 -6
- package/lib/module/ui/screens/PrivacySettingsScreen.js.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.user.d.ts +2 -2
- package/lib/typescript/core/mixins/OxyServices.user.d.ts.map +1 -1
- 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/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/useSessionSocket.d.ts.map +1 -1
- package/lib/typescript/ui/screens/PrivacySettingsScreen.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/mixins/OxyServices.user.ts +14 -4
- package/src/ui/context/OxyContext.tsx +310 -86
- package/src/ui/hooks/mutations/useAccountMutations.ts +8 -6
- package/src/ui/hooks/queries/useAccountQueries.ts +4 -2
- package/src/ui/hooks/useSessionSocket.ts +153 -155
- package/src/ui/screens/PrivacySettingsScreen.tsx +12 -6
|
@@ -59,9 +59,12 @@ export interface OxyContextState {
|
|
|
59
59
|
isIdentitySynced: () => Promise<boolean>;
|
|
60
60
|
syncIdentity: () => Promise<User>;
|
|
61
61
|
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
|
|
62
|
+
storeTransferCode: (transferId: string, code: string, sourceDeviceId: string | null, publicKey: string) => Promise<void>;
|
|
63
|
+
getTransferCode: (transferId: string) => { code: string; sourceDeviceId: string | null; publicKey: string; timestamp: number; state: 'pending' | 'completed' | 'failed' } | null;
|
|
64
|
+
clearTransferCode: (transferId: string) => Promise<void>;
|
|
65
|
+
getAllPendingTransfers: () => Array<{ transferId: string; data: { code: string; sourceDeviceId: string | null; publicKey: string; timestamp: number; state: 'pending' | 'completed' | 'failed' } }>;
|
|
66
|
+
getActiveTransferId: () => string | null;
|
|
67
|
+
updateTransferState: (transferId: string, state: 'pending' | 'completed' | 'failed') => Promise<void>;
|
|
65
68
|
|
|
66
69
|
// Identity sync state (reactive, from Zustand store)
|
|
67
70
|
identitySyncState: {
|
|
@@ -194,7 +197,11 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
194
197
|
|
|
195
198
|
const logger = useCallback((message: string, err?: unknown) => {
|
|
196
199
|
if (__DEV__) {
|
|
197
|
-
|
|
200
|
+
if (err !== undefined) {
|
|
201
|
+
console.warn(`[OxyContext] ${message}`, err);
|
|
202
|
+
} else {
|
|
203
|
+
console.warn(`[OxyContext] ${message}`);
|
|
204
|
+
}
|
|
198
205
|
}
|
|
199
206
|
}, []);
|
|
200
207
|
|
|
@@ -380,6 +387,21 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
380
387
|
oxyServices.clearCache();
|
|
381
388
|
}, [queryClient, storage, clearSessionState, logger, oxyServices]);
|
|
382
389
|
|
|
390
|
+
// Transfer code management functions (must be defined before deleteIdentityAndClearAccount)
|
|
391
|
+
const getAllPendingTransfers = useCallback(() => {
|
|
392
|
+
const pending: Array<{ transferId: string; data: TransferCodeData }> = [];
|
|
393
|
+
transferCodesRef.current.forEach((data, transferId) => {
|
|
394
|
+
if (data.state === 'pending') {
|
|
395
|
+
pending.push({ transferId, data });
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
return pending;
|
|
399
|
+
}, []);
|
|
400
|
+
|
|
401
|
+
const getActiveTransferId = useCallback(() => {
|
|
402
|
+
return activeTransferIdRef.current;
|
|
403
|
+
}, []);
|
|
404
|
+
|
|
383
405
|
// Delete identity and clear all account data
|
|
384
406
|
// In accounts app, deleting identity means losing the account completely
|
|
385
407
|
const deleteIdentityAndClearAccount = useCallback(async (
|
|
@@ -387,12 +409,30 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
387
409
|
force: boolean = false,
|
|
388
410
|
userConfirmed: boolean = false
|
|
389
411
|
): Promise<void> => {
|
|
412
|
+
// CRITICAL: Check for active transfers before deletion (unless force is true)
|
|
413
|
+
// This prevents accidental identity loss during transfer
|
|
414
|
+
if (!force) {
|
|
415
|
+
const pendingTransfers = getAllPendingTransfers();
|
|
416
|
+
if (pendingTransfers.length > 0) {
|
|
417
|
+
const activeTransferId = getActiveTransferId();
|
|
418
|
+
const hasActiveTransfer = activeTransferId && pendingTransfers.some(t => t.transferId === activeTransferId);
|
|
419
|
+
|
|
420
|
+
if (hasActiveTransfer) {
|
|
421
|
+
throw new Error(
|
|
422
|
+
'Cannot delete identity: An active identity transfer is in progress. ' +
|
|
423
|
+
'Please wait for the transfer to complete or cancel it first. ' +
|
|
424
|
+
'If you proceed, you may lose access to your identity permanently.'
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
390
430
|
// First, clear all account data
|
|
391
431
|
await clearAllAccountData();
|
|
392
432
|
|
|
393
433
|
// Then delete the identity keys
|
|
394
434
|
await KeyManager.deleteIdentity(skipBackup, force, userConfirmed);
|
|
395
|
-
}, [clearAllAccountData]);
|
|
435
|
+
}, [clearAllAccountData, getAllPendingTransfers, getActiveTransferId]);
|
|
396
436
|
|
|
397
437
|
// Network reconnect sync - TanStack Query automatically retries mutations on reconnect
|
|
398
438
|
// We only need to sync identity if it's not synced
|
|
@@ -401,6 +441,8 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
401
441
|
|
|
402
442
|
let wasOffline = false;
|
|
403
443
|
let checkTimeout: NodeJS.Timeout | null = null;
|
|
444
|
+
let lastReconnectionLog = 0;
|
|
445
|
+
const RECONNECTION_LOG_DEBOUNCE_MS = 5000; // 5 seconds
|
|
404
446
|
|
|
405
447
|
// Circuit breaker and exponential backoff state
|
|
406
448
|
const stateRef = {
|
|
@@ -436,35 +478,42 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
436
478
|
|
|
437
479
|
// If we were offline and now we're online, sync identity if needed
|
|
438
480
|
if (wasOffline) {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
481
|
+
const now = Date.now();
|
|
482
|
+
const timeSinceLastLog = now - lastReconnectionLog;
|
|
483
|
+
|
|
484
|
+
if (timeSinceLastLog >= RECONNECTION_LOG_DEBOUNCE_MS) {
|
|
485
|
+
logger('Network reconnected, checking identity sync...');
|
|
486
|
+
lastReconnectionLog = now;
|
|
487
|
+
|
|
488
|
+
// Sync identity first (if not synced)
|
|
489
|
+
try {
|
|
490
|
+
const hasIdentityValue = await hasIdentity();
|
|
491
|
+
if (hasIdentityValue) {
|
|
492
|
+
// Check sync status directly - sync if not explicitly 'true'
|
|
493
|
+
// undefined = not synced yet, 'false' = explicitly not synced, 'true' = synced
|
|
494
|
+
const syncStatus = await storage.getItem('oxy_identity_synced');
|
|
495
|
+
if (syncStatus !== 'true') {
|
|
496
|
+
await syncIdentity();
|
|
497
|
+
}
|
|
450
498
|
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
499
|
+
} catch (syncError: any) {
|
|
500
|
+
// Skip sync silently if username is required (expected when offline onboarding)
|
|
501
|
+
if (syncError?.code === 'USERNAME_REQUIRED' || syncError?.message === 'USERNAME_REQUIRED') {
|
|
502
|
+
if (__DEV__) {
|
|
503
|
+
loggerUtil.debug('Sync skipped - username required', { component: 'OxyContext', method: 'checkNetworkAndSync' }, syncError as unknown);
|
|
504
|
+
}
|
|
505
|
+
// Don't log or show error - username will be set later
|
|
506
|
+
} else if (!isTimeoutOrNetworkError(syncError)) {
|
|
507
|
+
// Only log unexpected errors - timeouts/network issues are expected when offline
|
|
508
|
+
logger('Error syncing identity on reconnect', syncError);
|
|
509
|
+
} else if (__DEV__) {
|
|
510
|
+
loggerUtil.debug('Identity sync timeout (expected when offline)', { component: 'OxyContext', method: 'checkNetworkAndSync' }, syncError as unknown);
|
|
457
511
|
}
|
|
458
|
-
// Don't log or show error - username will be set later
|
|
459
|
-
} else if (!isTimeoutOrNetworkError(syncError)) {
|
|
460
|
-
// Only log unexpected errors - timeouts/network issues are expected when offline
|
|
461
|
-
logger('Error syncing identity on reconnect', syncError);
|
|
462
|
-
} else if (__DEV__) {
|
|
463
|
-
loggerUtil.debug('Identity sync timeout (expected when offline)', { component: 'OxyContext', method: 'checkNetworkAndSync' }, syncError as unknown);
|
|
464
512
|
}
|
|
465
513
|
}
|
|
466
|
-
|
|
514
|
+
|
|
467
515
|
// TanStack Query will automatically retry pending mutations
|
|
516
|
+
// Reset flag immediately after processing (whether logged or not)
|
|
468
517
|
wasOffline = false;
|
|
469
518
|
}
|
|
470
519
|
} catch (error) {
|
|
@@ -621,51 +670,207 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
621
670
|
// The JWT token's userId field contains the MongoDB ObjectId
|
|
622
671
|
const userId = oxyServices.getCurrentUserId() || user?.id;
|
|
623
672
|
|
|
624
|
-
//
|
|
625
|
-
|
|
626
|
-
|
|
673
|
+
// Transfer code storage interface
|
|
674
|
+
interface TransferCodeData {
|
|
675
|
+
code: string;
|
|
676
|
+
sourceDeviceId: string | null;
|
|
677
|
+
publicKey: string;
|
|
678
|
+
timestamp: number;
|
|
679
|
+
state: 'pending' | 'completed' | 'failed';
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Store transfer codes in memory for verification (also persisted to storage)
|
|
683
|
+
// Map: transferId -> TransferCodeData
|
|
684
|
+
const transferCodesRef = useRef<Map<string, TransferCodeData>>(new Map());
|
|
685
|
+
const activeTransferIdRef = useRef<string | null>(null);
|
|
686
|
+
const TRANSFER_CODES_STORAGE_KEY = `${storageKeyPrefix}_transfer_codes`;
|
|
687
|
+
const ACTIVE_TRANSFER_STORAGE_KEY = `${storageKeyPrefix}_active_transfer_id`;
|
|
688
|
+
|
|
689
|
+
// Load transfer codes from storage on startup
|
|
690
|
+
useEffect(() => {
|
|
691
|
+
if (!storage || !isStorageReady) return;
|
|
692
|
+
|
|
693
|
+
const loadTransferCodes = async () => {
|
|
694
|
+
try {
|
|
695
|
+
// Load transfer codes
|
|
696
|
+
const storedCodes = await storage.getItem(TRANSFER_CODES_STORAGE_KEY);
|
|
697
|
+
if (storedCodes) {
|
|
698
|
+
const parsed = JSON.parse(storedCodes);
|
|
699
|
+
const now = Date.now();
|
|
700
|
+
const fifteenMinutes = 15 * 60 * 1000;
|
|
701
|
+
|
|
702
|
+
// Only restore non-expired pending transfers
|
|
703
|
+
Object.entries(parsed).forEach(([transferId, data]: [string, any]) => {
|
|
704
|
+
if (data.state === 'pending' && (now - data.timestamp) < fifteenMinutes) {
|
|
705
|
+
transferCodesRef.current.set(transferId, data);
|
|
706
|
+
if (__DEV__) {
|
|
707
|
+
logger('Restored pending transfer from storage', { transferId });
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Load active transfer ID
|
|
714
|
+
const activeTransferId = await storage.getItem(ACTIVE_TRANSFER_STORAGE_KEY);
|
|
715
|
+
if (activeTransferId) {
|
|
716
|
+
// Verify it's still valid
|
|
717
|
+
const transferData = transferCodesRef.current.get(activeTransferId);
|
|
718
|
+
if (transferData && transferData.state === 'pending') {
|
|
719
|
+
activeTransferIdRef.current = activeTransferId;
|
|
720
|
+
if (__DEV__) {
|
|
721
|
+
logger('Restored active transfer ID from storage', { transferId: activeTransferId });
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
// Clear invalid active transfer
|
|
725
|
+
await storage.removeItem(ACTIVE_TRANSFER_STORAGE_KEY);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
} catch (error) {
|
|
729
|
+
if (__DEV__) {
|
|
730
|
+
logger('Failed to load transfer codes from storage', error);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
loadTransferCodes();
|
|
736
|
+
}, [storage, isStorageReady, logger, storageKeyPrefix]);
|
|
737
|
+
|
|
738
|
+
// Persist transfer codes to storage whenever they change
|
|
739
|
+
const persistTransferCodes = useCallback(async () => {
|
|
740
|
+
if (!storage) return;
|
|
741
|
+
|
|
742
|
+
try {
|
|
743
|
+
const codesToStore: Record<string, TransferCodeData> = {};
|
|
744
|
+
transferCodesRef.current.forEach((value, key) => {
|
|
745
|
+
codesToStore[key] = value;
|
|
746
|
+
});
|
|
747
|
+
await storage.setItem(TRANSFER_CODES_STORAGE_KEY, JSON.stringify(codesToStore));
|
|
748
|
+
} catch (error) {
|
|
749
|
+
if (__DEV__) {
|
|
750
|
+
logger('Failed to persist transfer codes', error);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}, [storage, logger]);
|
|
627
754
|
|
|
628
755
|
// Cleanup old transfer codes (older than 15 minutes)
|
|
629
756
|
useEffect(() => {
|
|
630
|
-
const cleanup = setInterval(() => {
|
|
757
|
+
const cleanup = setInterval(async () => {
|
|
631
758
|
const now = Date.now();
|
|
632
759
|
const fifteenMinutes = 15 * 60 * 1000;
|
|
760
|
+
let needsPersist = false;
|
|
761
|
+
|
|
633
762
|
transferCodesRef.current.forEach((value, key) => {
|
|
634
763
|
if (now - value.timestamp > fifteenMinutes) {
|
|
635
764
|
transferCodesRef.current.delete(key);
|
|
765
|
+
needsPersist = true;
|
|
636
766
|
if (__DEV__) {
|
|
637
767
|
logger('Cleaned up expired transfer code', { transferId: key });
|
|
638
768
|
}
|
|
639
769
|
}
|
|
640
770
|
});
|
|
771
|
+
|
|
772
|
+
// Clear active transfer if it was deleted
|
|
773
|
+
if (activeTransferIdRef.current && !transferCodesRef.current.has(activeTransferIdRef.current)) {
|
|
774
|
+
activeTransferIdRef.current = null;
|
|
775
|
+
if (storage) {
|
|
776
|
+
try {
|
|
777
|
+
await storage.removeItem(ACTIVE_TRANSFER_STORAGE_KEY);
|
|
778
|
+
} catch (error) {
|
|
779
|
+
// Ignore storage errors
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (needsPersist) {
|
|
785
|
+
await persistTransferCodes();
|
|
786
|
+
}
|
|
641
787
|
}, 60000); // Check every minute
|
|
642
788
|
|
|
643
789
|
return () => clearInterval(cleanup);
|
|
644
|
-
}, [logger]);
|
|
790
|
+
}, [logger, persistTransferCodes, storage]);
|
|
645
791
|
|
|
646
792
|
// Transfer code management functions
|
|
647
|
-
const storeTransferCode = useCallback((transferId: string, code: string, sourceDeviceId: string | null, publicKey: string) => {
|
|
648
|
-
|
|
793
|
+
const storeTransferCode = useCallback(async (transferId: string, code: string, sourceDeviceId: string | null, publicKey: string) => {
|
|
794
|
+
const transferData: TransferCodeData = {
|
|
649
795
|
code,
|
|
650
796
|
sourceDeviceId,
|
|
651
797
|
publicKey,
|
|
652
798
|
timestamp: Date.now(),
|
|
653
|
-
|
|
799
|
+
state: 'pending',
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
transferCodesRef.current.set(transferId, transferData);
|
|
803
|
+
activeTransferIdRef.current = transferId;
|
|
804
|
+
|
|
805
|
+
// Persist to storage
|
|
806
|
+
await persistTransferCodes();
|
|
807
|
+
if (storage) {
|
|
808
|
+
try {
|
|
809
|
+
await storage.setItem(ACTIVE_TRANSFER_STORAGE_KEY, transferId);
|
|
810
|
+
} catch (error) {
|
|
811
|
+
if (__DEV__) {
|
|
812
|
+
logger('Failed to persist active transfer ID', error);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
654
817
|
if (__DEV__) {
|
|
655
818
|
logger('Stored transfer code', { transferId, sourceDeviceId, publicKey: publicKey.substring(0, 16) + '...' });
|
|
656
819
|
}
|
|
657
|
-
}, [logger]);
|
|
820
|
+
}, [logger, persistTransferCodes, storage]);
|
|
658
821
|
|
|
659
822
|
const getTransferCode = useCallback((transferId: string) => {
|
|
660
823
|
return transferCodesRef.current.get(transferId) || null;
|
|
661
824
|
}, []);
|
|
662
825
|
|
|
663
|
-
const
|
|
826
|
+
const updateTransferState = useCallback(async (transferId: string, state: 'pending' | 'completed' | 'failed') => {
|
|
827
|
+
const transferData = transferCodesRef.current.get(transferId);
|
|
828
|
+
if (transferData) {
|
|
829
|
+
transferData.state = state;
|
|
830
|
+
transferCodesRef.current.set(transferId, transferData);
|
|
831
|
+
|
|
832
|
+
// Clear active transfer if completed or failed
|
|
833
|
+
if (state === 'completed' || state === 'failed') {
|
|
834
|
+
if (activeTransferIdRef.current === transferId) {
|
|
835
|
+
activeTransferIdRef.current = null;
|
|
836
|
+
if (storage) {
|
|
837
|
+
try {
|
|
838
|
+
await storage.removeItem(ACTIVE_TRANSFER_STORAGE_KEY);
|
|
839
|
+
} catch (error) {
|
|
840
|
+
// Ignore storage errors
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
await persistTransferCodes();
|
|
847
|
+
|
|
848
|
+
if (__DEV__) {
|
|
849
|
+
logger('Updated transfer state', { transferId, state });
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}, [logger, persistTransferCodes, storage]);
|
|
853
|
+
|
|
854
|
+
const clearTransferCode = useCallback(async (transferId: string) => {
|
|
664
855
|
transferCodesRef.current.delete(transferId);
|
|
856
|
+
|
|
857
|
+
if (activeTransferIdRef.current === transferId) {
|
|
858
|
+
activeTransferIdRef.current = null;
|
|
859
|
+
if (storage) {
|
|
860
|
+
try {
|
|
861
|
+
await storage.removeItem(ACTIVE_TRANSFER_STORAGE_KEY);
|
|
862
|
+
} catch (error) {
|
|
863
|
+
// Ignore storage errors
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
await persistTransferCodes();
|
|
869
|
+
|
|
665
870
|
if (__DEV__) {
|
|
666
871
|
logger('Cleared transfer code', { transferId });
|
|
667
872
|
}
|
|
668
|
-
}, [logger]);
|
|
873
|
+
}, [logger, persistTransferCodes, storage]);
|
|
669
874
|
|
|
670
875
|
const refreshSessionsWithUser = useCallback(
|
|
671
876
|
() => refreshSessions(userId),
|
|
@@ -686,15 +891,6 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
686
891
|
|
|
687
892
|
const handleIdentityTransferComplete = useCallback(
|
|
688
893
|
async (data: { transferId: string; sourceDeviceId: string; publicKey: string; transferCode?: string; completedAt: string }) => {
|
|
689
|
-
if (__DEV__) {
|
|
690
|
-
console.log('[OxyContext] handleIdentityTransferComplete called', {
|
|
691
|
-
transferId: data.transferId,
|
|
692
|
-
sourceDeviceId: data.sourceDeviceId,
|
|
693
|
-
currentDeviceId,
|
|
694
|
-
hasActiveSession: activeSessionId !== null,
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
|
|
698
894
|
try {
|
|
699
895
|
logger('Received identity transfer complete notification', {
|
|
700
896
|
transferId: data.transferId,
|
|
@@ -704,15 +900,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
704
900
|
publicKey: data.publicKey.substring(0, 16) + '...',
|
|
705
901
|
});
|
|
706
902
|
|
|
707
|
-
// Get stored transfer code for verification
|
|
708
903
|
const storedTransfer = getTransferCode(data.transferId);
|
|
709
904
|
|
|
710
905
|
if (!storedTransfer) {
|
|
711
|
-
if (__DEV__) {
|
|
712
|
-
console.warn('[OxyContext] Transfer code not found for transferId', {
|
|
713
|
-
transferId: data.transferId,
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
906
|
logger('Transfer code not found for transferId', {
|
|
717
907
|
transferId: data.transferId,
|
|
718
908
|
});
|
|
@@ -720,15 +910,6 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
720
910
|
return;
|
|
721
911
|
}
|
|
722
912
|
|
|
723
|
-
if (__DEV__) {
|
|
724
|
-
console.log('[OxyContext] Found stored transfer code', {
|
|
725
|
-
transferId: data.transferId,
|
|
726
|
-
storedSourceDeviceId: storedTransfer.sourceDeviceId,
|
|
727
|
-
receivedSourceDeviceId: data.sourceDeviceId,
|
|
728
|
-
currentDeviceId,
|
|
729
|
-
});
|
|
730
|
-
}
|
|
731
|
-
|
|
732
913
|
// Verify publicKey matches first (most important check)
|
|
733
914
|
const publicKeyMatches = data.publicKey === storedTransfer.publicKey;
|
|
734
915
|
if (!publicKeyMatches) {
|
|
@@ -784,13 +965,6 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
784
965
|
// Don't block - publicKey match is sufficient, code mismatch might be due to user error
|
|
785
966
|
// Log warning but proceed
|
|
786
967
|
}
|
|
787
|
-
} else {
|
|
788
|
-
// If transfer code is not provided, log info but proceed
|
|
789
|
-
if (__DEV__) {
|
|
790
|
-
logger('Transfer code not provided in completion notification, but publicKey matches - proceeding', {
|
|
791
|
-
transferId: data.transferId
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
968
|
}
|
|
795
969
|
|
|
796
970
|
// Check if transfer is too old (safety timeout - 10 minutes)
|
|
@@ -807,15 +981,44 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
807
981
|
return;
|
|
808
982
|
}
|
|
809
983
|
|
|
810
|
-
//
|
|
811
|
-
|
|
812
|
-
|
|
984
|
+
// CRITICAL: Verify target device has identity before deleting on source device
|
|
985
|
+
// This ensures we never lose the identity
|
|
986
|
+
try {
|
|
987
|
+
logger('Verifying target device has identity before deletion', {
|
|
813
988
|
transferId: data.transferId,
|
|
814
|
-
|
|
815
|
-
currentDeviceId,
|
|
989
|
+
publicKey: data.publicKey.substring(0, 16) + '...',
|
|
816
990
|
});
|
|
991
|
+
|
|
992
|
+
// Verify target device has active session with matching public key
|
|
993
|
+
const verifyResponse = await oxyServices.makeRequest<{ verified: boolean; hasActiveSession: boolean }>(
|
|
994
|
+
'GET',
|
|
995
|
+
`/api/identity/verify-transfer?publicKey=${encodeURIComponent(data.publicKey)}`,
|
|
996
|
+
undefined,
|
|
997
|
+
{ cache: false }
|
|
998
|
+
);
|
|
999
|
+
|
|
1000
|
+
if (!verifyResponse.verified || !verifyResponse.hasActiveSession) {
|
|
1001
|
+
logger('Target device verification failed - identity will not be deleted', {
|
|
1002
|
+
transferId: data.transferId,
|
|
1003
|
+
verified: verifyResponse.verified,
|
|
1004
|
+
hasActiveSession: verifyResponse.hasActiveSession,
|
|
1005
|
+
});
|
|
1006
|
+
await updateTransferState(data.transferId, 'failed');
|
|
1007
|
+
toast.error('Transfer verification failed: Target device does not have active session. Identity will not be deleted.');
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
logger('Target device verification passed', {
|
|
1012
|
+
transferId: data.transferId,
|
|
1013
|
+
});
|
|
1014
|
+
} catch (verifyError: any) {
|
|
1015
|
+
// If verification fails, don't delete identity - it's safer to keep it
|
|
1016
|
+
logger('Failed to verify target device - identity will not be deleted', verifyError);
|
|
1017
|
+
await updateTransferState(data.transferId, 'failed');
|
|
1018
|
+
toast.error('Transfer verification failed: Could not verify target device. Identity will not be deleted.');
|
|
1019
|
+
return;
|
|
817
1020
|
}
|
|
818
|
-
|
|
1021
|
+
|
|
819
1022
|
logger('All transfer verifications passed, deleting identity from source device', {
|
|
820
1023
|
transferId: data.transferId,
|
|
821
1024
|
sourceDeviceId: data.sourceDeviceId,
|
|
@@ -823,14 +1026,31 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
823
1026
|
});
|
|
824
1027
|
|
|
825
1028
|
try {
|
|
1029
|
+
// Verify identity still exists before deletion (safety check)
|
|
1030
|
+
const identityStillExists = await KeyManager.hasIdentity();
|
|
1031
|
+
if (!identityStillExists) {
|
|
1032
|
+
logger('Identity already deleted - skipping deletion', {
|
|
1033
|
+
transferId: data.transferId,
|
|
1034
|
+
});
|
|
1035
|
+
await updateTransferState(data.transferId, 'completed');
|
|
1036
|
+
await clearTransferCode(data.transferId);
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
826
1040
|
await deleteIdentityAndClearAccount(false, false, true);
|
|
827
1041
|
|
|
828
|
-
|
|
829
|
-
|
|
1042
|
+
// Verify identity was actually deleted
|
|
1043
|
+
const identityDeleted = !(await KeyManager.hasIdentity());
|
|
1044
|
+
if (!identityDeleted) {
|
|
1045
|
+
logger('Identity deletion failed - identity still exists', {
|
|
1046
|
+
transferId: data.transferId,
|
|
1047
|
+
});
|
|
1048
|
+
await updateTransferState(data.transferId, 'failed');
|
|
1049
|
+
throw new Error('Identity deletion failed - identity still exists');
|
|
830
1050
|
}
|
|
831
1051
|
|
|
832
|
-
|
|
833
|
-
clearTransferCode(data.transferId);
|
|
1052
|
+
await updateTransferState(data.transferId, 'completed');
|
|
1053
|
+
await clearTransferCode(data.transferId);
|
|
834
1054
|
|
|
835
1055
|
logger('Identity successfully deleted and transfer code cleared', {
|
|
836
1056
|
transferId: data.transferId,
|
|
@@ -838,18 +1058,16 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
838
1058
|
|
|
839
1059
|
toast.success('Identity successfully transferred and removed from this device');
|
|
840
1060
|
} catch (deleteError: any) {
|
|
841
|
-
if (__DEV__) {
|
|
842
|
-
console.error('[OxyContext] Error deleting identity', deleteError);
|
|
843
|
-
}
|
|
844
1061
|
logger('Error during identity deletion', deleteError);
|
|
845
|
-
|
|
1062
|
+
await updateTransferState(data.transferId, 'failed');
|
|
1063
|
+
throw deleteError;
|
|
846
1064
|
}
|
|
847
1065
|
} catch (error: any) {
|
|
848
1066
|
logger('Failed to delete identity after transfer', error);
|
|
849
1067
|
toast.error(error?.message || 'Failed to remove identity from this device. Please try again manually from Security Settings.');
|
|
850
1068
|
}
|
|
851
1069
|
},
|
|
852
|
-
[deleteIdentityAndClearAccount, logger, getTransferCode, clearTransferCode, currentDeviceId, activeSessionId],
|
|
1070
|
+
[deleteIdentityAndClearAccount, logger, getTransferCode, clearTransferCode, updateTransferState, currentDeviceId, activeSessionId, oxyServices],
|
|
853
1071
|
);
|
|
854
1072
|
|
|
855
1073
|
useSessionSocket({
|
|
@@ -943,6 +1161,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
943
1161
|
storeTransferCode,
|
|
944
1162
|
getTransferCode,
|
|
945
1163
|
clearTransferCode,
|
|
1164
|
+
getAllPendingTransfers,
|
|
1165
|
+
getActiveTransferId,
|
|
1166
|
+
updateTransferState,
|
|
946
1167
|
identitySyncState: {
|
|
947
1168
|
isSynced: isIdentitySyncedStore ?? true,
|
|
948
1169
|
isSyncing: isSyncing ?? false,
|
|
@@ -976,6 +1197,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
976
1197
|
storeTransferCode,
|
|
977
1198
|
getTransferCode,
|
|
978
1199
|
clearTransferCode,
|
|
1200
|
+
getAllPendingTransfers,
|
|
1201
|
+
getActiveTransferId,
|
|
1202
|
+
updateTransferState,
|
|
979
1203
|
isIdentitySyncedStore,
|
|
980
1204
|
isSyncing,
|
|
981
1205
|
currentLanguage,
|
|
@@ -292,12 +292,14 @@ export const useUpdateAccountSettings = () => {
|
|
|
292
292
|
* Update privacy settings with optimistic updates and authentication handling
|
|
293
293
|
*/
|
|
294
294
|
export const useUpdatePrivacySettings = () => {
|
|
295
|
-
const { oxyServices, activeSessionId,
|
|
295
|
+
const { oxyServices, activeSessionId, syncIdentity } = useOxy();
|
|
296
296
|
const queryClient = useQueryClient();
|
|
297
297
|
|
|
298
298
|
return useMutation({
|
|
299
299
|
mutationFn: async ({ settings, userId }: { settings: Record<string, any>; userId?: string }) => {
|
|
300
|
-
|
|
300
|
+
// Use getCurrentUserId() which returns MongoDB ObjectId from JWT token
|
|
301
|
+
// Never use user?.id as it may be set to publicKey
|
|
302
|
+
const targetUserId = userId || oxyServices.getCurrentUserId();
|
|
301
303
|
if (!targetUserId) {
|
|
302
304
|
throw new Error('User ID is required');
|
|
303
305
|
}
|
|
@@ -353,7 +355,7 @@ export const useUpdatePrivacySettings = () => {
|
|
|
353
355
|
},
|
|
354
356
|
// Optimistic update
|
|
355
357
|
onMutate: async ({ settings, userId }) => {
|
|
356
|
-
const targetUserId = userId ||
|
|
358
|
+
const targetUserId = userId || oxyServices.getCurrentUserId();
|
|
357
359
|
if (!targetUserId) return;
|
|
358
360
|
|
|
359
361
|
// Cancel outgoing refetches
|
|
@@ -387,7 +389,7 @@ export const useUpdatePrivacySettings = () => {
|
|
|
387
389
|
},
|
|
388
390
|
// On error, rollback
|
|
389
391
|
onError: (error, { userId }, context) => {
|
|
390
|
-
const targetUserId = userId ||
|
|
392
|
+
const targetUserId = userId || oxyServices.getCurrentUserId();
|
|
391
393
|
if (context?.previousPrivacySettings && targetUserId) {
|
|
392
394
|
queryClient.setQueryData(queryKeys.privacy.settings(targetUserId), context.previousPrivacySettings);
|
|
393
395
|
}
|
|
@@ -398,7 +400,7 @@ export const useUpdatePrivacySettings = () => {
|
|
|
398
400
|
},
|
|
399
401
|
// On success, invalidate and refetch
|
|
400
402
|
onSuccess: (data, { userId }) => {
|
|
401
|
-
const targetUserId = userId ||
|
|
403
|
+
const targetUserId = userId || oxyServices.getCurrentUserId();
|
|
402
404
|
if (targetUserId) {
|
|
403
405
|
queryClient.setQueryData(queryKeys.privacy.settings(targetUserId), data);
|
|
404
406
|
}
|
|
@@ -418,7 +420,7 @@ export const useUpdatePrivacySettings = () => {
|
|
|
418
420
|
},
|
|
419
421
|
// Always refetch after error or success
|
|
420
422
|
onSettled: (data, error, { userId }) => {
|
|
421
|
-
const targetUserId = userId ||
|
|
423
|
+
const targetUserId = userId || oxyServices.getCurrentUserId();
|
|
422
424
|
if (targetUserId) {
|
|
423
425
|
queryClient.invalidateQueries({ queryKey: queryKeys.privacy.settings(targetUserId) });
|
|
424
426
|
}
|
|
@@ -128,8 +128,10 @@ export const useUsersBySessions = (sessionIds: string[], options?: { enabled?: b
|
|
|
128
128
|
* Get privacy settings for a user
|
|
129
129
|
*/
|
|
130
130
|
export const usePrivacySettings = (userId?: string, options?: { enabled?: boolean }) => {
|
|
131
|
-
const { oxyServices, activeSessionId, syncIdentity
|
|
132
|
-
|
|
131
|
+
const { oxyServices, activeSessionId, syncIdentity } = useOxy();
|
|
132
|
+
// Use getCurrentUserId() which returns MongoDB ObjectId from JWT token
|
|
133
|
+
// Never use user?.id as it may be set to publicKey
|
|
134
|
+
const targetUserId = userId || oxyServices.getCurrentUserId() || undefined;
|
|
133
135
|
|
|
134
136
|
return useQuery({
|
|
135
137
|
queryKey: queryKeys.privacy.settings(targetUserId),
|