@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.
Files changed (39) hide show
  1. package/lib/commonjs/core/mixins/OxyServices.user.js +14 -4
  2. package/lib/commonjs/core/mixins/OxyServices.user.js.map +1 -1
  3. package/lib/commonjs/ui/context/OxyContext.js +280 -84
  4. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  5. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +7 -6
  6. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  7. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +4 -3
  8. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -1
  9. package/lib/commonjs/ui/hooks/useSessionSocket.js +349 -328
  10. package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
  11. package/lib/commonjs/ui/screens/PrivacySettingsScreen.js +13 -6
  12. package/lib/commonjs/ui/screens/PrivacySettingsScreen.js.map +1 -1
  13. package/lib/module/core/mixins/OxyServices.user.js +14 -4
  14. package/lib/module/core/mixins/OxyServices.user.js.map +1 -1
  15. package/lib/module/ui/context/OxyContext.js +280 -84
  16. package/lib/module/ui/context/OxyContext.js.map +1 -1
  17. package/lib/module/ui/hooks/mutations/useAccountMutations.js +7 -6
  18. package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  19. package/lib/module/ui/hooks/queries/useAccountQueries.js +4 -3
  20. package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -1
  21. package/lib/module/ui/hooks/useSessionSocket.js +349 -328
  22. package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
  23. package/lib/module/ui/screens/PrivacySettingsScreen.js +13 -6
  24. package/lib/module/ui/screens/PrivacySettingsScreen.js.map +1 -1
  25. package/lib/typescript/core/mixins/OxyServices.user.d.ts +2 -2
  26. package/lib/typescript/core/mixins/OxyServices.user.d.ts.map +1 -1
  27. package/lib/typescript/ui/context/OxyContext.d.ts +15 -2
  28. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  29. package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  30. package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
  31. package/lib/typescript/ui/hooks/useSessionSocket.d.ts.map +1 -1
  32. package/lib/typescript/ui/screens/PrivacySettingsScreen.d.ts.map +1 -1
  33. package/package.json +1 -1
  34. package/src/core/mixins/OxyServices.user.ts +14 -4
  35. package/src/ui/context/OxyContext.tsx +310 -86
  36. package/src/ui/hooks/mutations/useAccountMutations.ts +8 -6
  37. package/src/ui/hooks/queries/useAccountQueries.ts +4 -2
  38. package/src/ui/hooks/useSessionSocket.ts +153 -155
  39. 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
- console.warn(`[OxyContext] ${message}`, err);
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
- logger('Network reconnected, checking identity sync...');
440
-
441
- // Sync identity first (if not synced)
442
- try {
443
- const hasIdentityValue = await hasIdentity();
444
- if (hasIdentityValue) {
445
- // Check sync status directly - sync if not explicitly 'true'
446
- // undefined = not synced yet, 'false' = explicitly not synced, 'true' = synced
447
- const syncStatus = await storage.getItem('oxy_identity_synced');
448
- if (syncStatus !== 'true') {
449
- await syncIdentity();
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
- } catch (syncError: any) {
453
- // Skip sync silently if username is required (expected when offline onboarding)
454
- if (syncError?.code === 'USERNAME_REQUIRED' || syncError?.message === 'USERNAME_REQUIRED') {
455
- if (__DEV__) {
456
- loggerUtil.debug('Sync skipped - username required', { component: 'OxyContext', method: 'checkNetworkAndSync' }, syncError as unknown);
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
- // Store transfer codes in memory for verification
625
- // Map: transferId -> { code, sourceDeviceId, publicKey, timestamp }
626
- const transferCodesRef = useRef<Map<string, { code: string; sourceDeviceId: string | null; publicKey: string; timestamp: number }>>(new Map());
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
- transferCodesRef.current.set(transferId, {
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 clearTransferCode = useCallback((transferId: string) => {
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
- // All verifications passed - automatically delete identity
811
- if (__DEV__) {
812
- console.log('[OxyContext] All verifications passed, deleting identity', {
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
- sourceDeviceId: data.sourceDeviceId,
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
- if (__DEV__) {
829
- console.log('[OxyContext] Identity deleted successfully');
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
- // Clear the transfer code after successful deletion
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
- throw deleteError; // Re-throw to be caught by outer catch
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, user, syncIdentity } = useOxy();
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
- const targetUserId = userId || user?.id;
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 || user?.id;
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 || user?.id;
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 || user?.id;
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 || user?.id;
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, user } = useOxy();
132
- const targetUserId = userId || user?.id;
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),