@oxyhq/services 5.17.7 → 5.17.9

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 (119) hide show
  1. package/lib/commonjs/crypto/index.js +0 -23
  2. package/lib/commonjs/crypto/index.js.map +1 -1
  3. package/lib/commonjs/index.js +0 -15
  4. package/lib/commonjs/index.js.map +1 -1
  5. package/lib/commonjs/ui/components/Icon.js.map +1 -1
  6. package/lib/commonjs/ui/components/IconButton/utils.js.map +1 -1
  7. package/lib/commonjs/ui/components/TextField/Adornment/utils.js.map +1 -1
  8. package/lib/commonjs/ui/components/TextField/helpers.js.map +1 -1
  9. package/lib/commonjs/ui/components/TouchableRipple/utils.js.map +1 -1
  10. package/lib/commonjs/ui/components/Typography/AnimatedText.js.map +1 -1
  11. package/lib/commonjs/ui/context/OxyContext.js +37 -589
  12. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  13. package/lib/commonjs/ui/context/OxyContextBase.js.map +1 -1
  14. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +60 -425
  15. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  16. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +8 -112
  17. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  18. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +2 -27
  19. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -1
  20. package/lib/commonjs/ui/hooks/queries/useServicesQueries.js +2 -27
  21. package/lib/commonjs/ui/hooks/queries/useServicesQueries.js.map +1 -1
  22. package/lib/commonjs/ui/hooks/useSessionSocket.js +2 -88
  23. package/lib/commonjs/ui/hooks/useSessionSocket.js.map +1 -1
  24. package/lib/commonjs/ui/screens/OxyAuthScreen.js +0 -1
  25. package/lib/commonjs/ui/screens/OxyAuthScreen.js.map +1 -1
  26. package/lib/commonjs/ui/stores/authStore.js +52 -15
  27. package/lib/commonjs/ui/stores/authStore.js.map +1 -1
  28. package/lib/commonjs/ui/utils/avatarUtils.js +2 -32
  29. package/lib/commonjs/ui/utils/avatarUtils.js.map +1 -1
  30. package/lib/module/crypto/index.js +4 -6
  31. package/lib/module/crypto/index.js.map +1 -1
  32. package/lib/module/index.js +6 -3
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/ui/components/Icon.js.map +1 -1
  35. package/lib/module/ui/components/IconButton/utils.js.map +1 -1
  36. package/lib/module/ui/components/TextField/Adornment/utils.js.map +1 -1
  37. package/lib/module/ui/components/TextField/helpers.js.map +1 -1
  38. package/lib/module/ui/components/TouchableRipple/utils.js.map +1 -1
  39. package/lib/module/ui/components/Typography/AnimatedText.js.map +1 -1
  40. package/lib/module/ui/context/OxyContext.js +35 -588
  41. package/lib/module/ui/context/OxyContext.js.map +1 -1
  42. package/lib/module/ui/context/OxyContextBase.js.map +1 -1
  43. package/lib/module/ui/context/hooks/useAuthOperations.js +60 -424
  44. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  45. package/lib/module/ui/hooks/mutations/useAccountMutations.js +8 -112
  46. package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -1
  47. package/lib/module/ui/hooks/queries/useAccountQueries.js +2 -27
  48. package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -1
  49. package/lib/module/ui/hooks/queries/useServicesQueries.js +2 -27
  50. package/lib/module/ui/hooks/queries/useServicesQueries.js.map +1 -1
  51. package/lib/module/ui/hooks/useSessionSocket.js +2 -88
  52. package/lib/module/ui/hooks/useSessionSocket.js.map +1 -1
  53. package/lib/module/ui/screens/OxyAuthScreen.js +0 -1
  54. package/lib/module/ui/screens/OxyAuthScreen.js.map +1 -1
  55. package/lib/module/ui/stores/authStore.js +52 -15
  56. package/lib/module/ui/stores/authStore.js.map +1 -1
  57. package/lib/module/ui/utils/avatarUtils.js +2 -32
  58. package/lib/module/ui/utils/avatarUtils.js.map +1 -1
  59. package/lib/typescript/crypto/index.d.ts +2 -5
  60. package/lib/typescript/crypto/index.d.ts.map +1 -1
  61. package/lib/typescript/crypto/types.d.ts +6 -2
  62. package/lib/typescript/crypto/types.d.ts.map +1 -1
  63. package/lib/typescript/index.d.ts +4 -2
  64. package/lib/typescript/index.d.ts.map +1 -1
  65. package/lib/typescript/ui/components/IconButton/utils.d.ts +1 -1
  66. package/lib/typescript/ui/components/TextField/Adornment/utils.d.ts +1 -1
  67. package/lib/typescript/ui/components/TextField/Adornment/utils.d.ts.map +1 -1
  68. package/lib/typescript/ui/components/TextField/helpers.d.ts +6 -6
  69. package/lib/typescript/ui/components/types.d.ts +0 -4
  70. package/lib/typescript/ui/components/types.d.ts.map +1 -1
  71. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  72. package/lib/typescript/ui/context/OxyContextBase.d.ts +2 -39
  73. package/lib/typescript/ui/context/OxyContextBase.d.ts.map +1 -1
  74. package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts +10 -25
  75. package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  76. package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  77. package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
  78. package/lib/typescript/ui/hooks/queries/useServicesQueries.d.ts.map +1 -1
  79. package/lib/typescript/ui/hooks/useSessionSocket.d.ts +1 -14
  80. package/lib/typescript/ui/hooks/useSessionSocket.d.ts.map +1 -1
  81. package/lib/typescript/ui/stores/authStore.d.ts +27 -4
  82. package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
  83. package/lib/typescript/ui/utils/avatarUtils.d.ts +0 -2
  84. package/lib/typescript/ui/utils/avatarUtils.d.ts.map +1 -1
  85. package/package.json +2 -2
  86. package/src/crypto/index.ts +3 -11
  87. package/src/crypto/types.ts +6 -2
  88. package/src/index.ts +6 -11
  89. package/src/ui/components/Icon.tsx +1 -1
  90. package/src/ui/components/IconButton/utils.ts +1 -1
  91. package/src/ui/components/TextField/Adornment/utils.ts +2 -2
  92. package/src/ui/components/TextField/helpers.tsx +8 -8
  93. package/src/ui/components/TouchableRipple/utils.ts +2 -2
  94. package/src/ui/components/Typography/AnimatedText.tsx +2 -2
  95. package/src/ui/components/types.tsx +0 -6
  96. package/src/ui/context/OxyContext.tsx +33 -637
  97. package/src/ui/context/OxyContextBase.tsx +5 -23
  98. package/src/ui/context/hooks/useAuthOperations.ts +84 -460
  99. package/src/ui/hooks/mutations/useAccountMutations.ts +12 -110
  100. package/src/ui/hooks/queries/useAccountQueries.ts +3 -27
  101. package/src/ui/hooks/queries/useServicesQueries.ts +3 -27
  102. package/src/ui/hooks/useSessionSocket.ts +2 -106
  103. package/src/ui/screens/OxyAuthScreen.tsx +1 -1
  104. package/src/ui/stores/authStore.ts +57 -18
  105. package/src/ui/utils/avatarUtils.ts +4 -36
  106. package/lib/commonjs/crypto/keyManager.js +0 -511
  107. package/lib/commonjs/crypto/keyManager.js.map +0 -1
  108. package/lib/commonjs/crypto/signatureService.js +0 -269
  109. package/lib/commonjs/crypto/signatureService.js.map +0 -1
  110. package/lib/module/crypto/keyManager.js +0 -508
  111. package/lib/module/crypto/keyManager.js.map +0 -1
  112. package/lib/module/crypto/signatureService.js +0 -266
  113. package/lib/module/crypto/signatureService.js.map +0 -1
  114. package/lib/typescript/crypto/keyManager.d.ts +0 -97
  115. package/lib/typescript/crypto/keyManager.d.ts.map +0 -1
  116. package/lib/typescript/crypto/signatureService.d.ts +0 -77
  117. package/lib/typescript/crypto/signatureService.d.ts.map +0 -1
  118. package/src/crypto/keyManager.ts +0 -545
  119. package/src/crypto/signatureService.ts +0 -301
@@ -27,7 +27,7 @@ import { showBottomSheet as globalShowBottomSheet } from '../navigation/bottomSh
27
27
  import { useQueryClient, useQuery } from '@tanstack/react-query';
28
28
  import { clearQueryCache } from '../hooks/queryClient';
29
29
  import { queryKeys } from '../hooks/queries/queryKeys';
30
- import { KeyManager, type BackupData } from '../../crypto';
30
+ import type { BackupData } from '../../crypto';
31
31
  import { translate } from '../../i18n';
32
32
  import { updateAvatarVisibility, updateProfileWithAvatar } from '../utils/avatarUtils';
33
33
  import { useAccountStore } from '../stores/accountStore';
@@ -74,31 +74,6 @@ const loadUseFollowHook = (): UseFollowHook => {
74
74
  }
75
75
  };
76
76
 
77
- // Shared storage key for identity cache - used by accounts app for instant routing
78
- // Uses expo-secure-store for consistency with identity storage (KeyManager)
79
- const IDENTITY_CACHE_KEY = 'oxy_identity_exists_cache';
80
-
81
- // Helper to update identity cache in SecureStore
82
- const updateIdentityCache = async (exists: boolean): Promise<void> => {
83
- if (Platform.OS === 'web') return;
84
- try {
85
- const SecureStore = await import('expo-secure-store');
86
- await SecureStore.setItemAsync(IDENTITY_CACHE_KEY, exists ? 'true' : 'false');
87
- } catch {
88
- // Silently fail - cache is just an optimization
89
- }
90
- };
91
-
92
- const clearIdentityCache = async (): Promise<void> => {
93
- if (Platform.OS === 'web') return;
94
- try {
95
- const SecureStore = await import('expo-secure-store');
96
- await SecureStore.deleteItemAsync(IDENTITY_CACHE_KEY);
97
- } catch {
98
- // Silently fail
99
- }
100
- };
101
-
102
77
  export const OxyProvider: React.FC<OxyContextProviderProps> = ({
103
78
  children,
104
79
  oxyServices: providedOxyServices,
@@ -123,29 +98,23 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
123
98
 
124
99
  const {
125
100
  isAuthenticated,
101
+ isOnline,
126
102
  isLoading,
127
103
  error,
104
+ setOnline,
128
105
  loginSuccess,
129
106
  loginFailure,
130
107
  logoutStore,
131
- // Identity sync state and actions
132
- isIdentitySyncedStore,
133
- isSyncing,
134
- setIdentitySynced,
135
- setSyncing,
136
108
  } = useAuthStore(
137
109
  useShallow((state: AuthState) => ({
138
110
  isAuthenticated: state.isAuthenticated,
111
+ isOnline: state.isOnline,
139
112
  isLoading: state.isLoading,
140
113
  error: state.error,
114
+ setOnline: state.setOnline,
141
115
  loginSuccess: state.loginSuccess,
142
116
  loginFailure: state.loginFailure,
143
117
  logoutStore: state.logout,
144
- // Identity sync state and actions
145
- isIdentitySyncedStore: state.isIdentitySynced,
146
- isSyncing: state.isSyncing,
147
- setIdentitySynced: state.setIdentitySynced,
148
- setSyncing: state.setSyncing,
149
118
  })),
150
119
  );
151
120
 
@@ -167,52 +136,6 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
167
136
 
168
137
  const { storage, isReady: isStorageReady } = useStorage({ onError, logger });
169
138
 
170
- // Identity integrity check and auto-restore on startup
171
- // Skip on web platform - identity storage is only available on native platforms
172
- useEffect(() => {
173
- if (!storage || !isStorageReady) return;
174
- if (Platform.OS === 'web') return; // Identity operations are native-only
175
-
176
- const checkAndRestoreIdentity = async () => {
177
- try {
178
- // Check if identity exists and verify integrity
179
- const hasIdentity = await KeyManager.hasIdentity();
180
- if (hasIdentity) {
181
- const isValid = await KeyManager.verifyIdentityIntegrity();
182
- if (!isValid) {
183
- // Try to restore from backup
184
- const restored = await KeyManager.restoreIdentityFromBackup();
185
- if (__DEV__) {
186
- logger(restored
187
- ? 'Identity restored from backup successfully'
188
- : 'Identity integrity check failed - user may need to restore from backup file'
189
- );
190
- }
191
- } else {
192
- // Identity is valid - ensure backup is up to date
193
- await KeyManager.backupIdentity();
194
- }
195
- } else {
196
- // No identity - try to restore from backup
197
- const restored = await KeyManager.restoreIdentityFromBackup();
198
- if (restored && __DEV__) {
199
- logger('Identity restored from backup on startup');
200
- }
201
- }
202
- } catch (error) {
203
- if (__DEV__) {
204
- logger('Error during identity integrity check', error);
205
- }
206
- // Don't block app startup - user can recover with backup file
207
- }
208
- };
209
-
210
- checkAndRestoreIdentity();
211
- }, [storage, isStorageReady, logger]);
212
-
213
- // Offline queuing is now handled by TanStack Query mutations
214
- // No need for custom offline queue
215
-
216
139
  const {
217
140
  currentLanguage,
218
141
  metadata: currentLanguageMetadata,
@@ -274,15 +197,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
274
197
  const user = userData ?? null;
275
198
 
276
199
  const {
277
- createIdentity,
278
- importIdentity: importIdentityBase,
279
- signIn,
200
+ completeSignIn,
280
201
  logout,
281
202
  logoutAll,
282
- hasIdentity,
283
- getPublicKey,
284
- isIdentitySynced,
285
- syncIdentity: syncIdentityBase,
286
203
  } = useAuthOperations({
287
204
  oxyServices,
288
205
  storage,
@@ -300,48 +217,10 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
300
217
  loginFailure,
301
218
  logoutStore,
302
219
  setAuthState,
303
- setIdentitySynced,
304
- setSyncing,
305
220
  logger,
306
221
  });
307
222
 
308
- // syncIdentity - TanStack Query handles offline mutations automatically
309
- const syncIdentity = useCallback(() => syncIdentityBase(), [syncIdentityBase]);
310
-
311
- // Wrapper for createIdentity to update identity cache
312
- const wrappedCreateIdentity = useCallback(
313
- async (): Promise<{ synced: boolean }> => {
314
- const result = await createIdentity();
315
- // Update cache for instant routing on next app launch
316
- updateIdentityCache(true);
317
- return result;
318
- },
319
- [createIdentity]
320
- );
321
-
322
- // Wrapper for importIdentity to handle legacy calls and update cache
323
- const importIdentity = useCallback(
324
- async (backupData: BackupData | string, password?: string): Promise<{ synced: boolean }> => {
325
- // Handle legacy calls with single string argument (old recovery phrase signature)
326
- if (typeof backupData === 'string') {
327
- throw new Error('Recovery phrase import is no longer supported. Please use backup file import or QR code transfer instead.');
328
- }
329
-
330
- // Validate that password is provided
331
- if (!password || typeof password !== 'string') {
332
- throw new Error('Password is required for backup file import.');
333
- }
334
-
335
- const result = await importIdentityBase(backupData, password);
336
- // Update cache for instant routing on next app launch
337
- updateIdentityCache(true);
338
- return result;
339
- },
340
- [importIdentityBase]
341
- );
342
-
343
- // Clear all account data when identity is lost (for accounts app)
344
- // In accounts app, identity = account, so losing identity means losing everything
223
+ // Clear all cached data and storage on logout
345
224
  const clearAllAccountData = useCallback(async (): Promise<void> => {
346
225
  // Clear TanStack Query cache (in-memory)
347
226
  queryClient.clear();
@@ -358,19 +237,6 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
358
237
  // Clear session state (sessions, activeSessionId, storage)
359
238
  await clearSessionState();
360
239
 
361
- // Clear identity sync state from storage
362
- if (storage) {
363
- try {
364
- await storage.removeItem('oxy_identity_synced');
365
- } catch (error) {
366
- logger('Failed to clear identity sync state', error);
367
- }
368
- }
369
-
370
- // Reset auth store identity sync state
371
- useAuthStore.getState().setIdentitySynced(false);
372
- useAuthStore.getState().setSyncing(false);
373
-
374
240
  // Reset account store
375
241
  useAccountStore.getState().reset();
376
242
 
@@ -378,174 +244,63 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
378
244
  oxyServices.clearCache();
379
245
  }, [queryClient, storage, clearSessionState, logger, oxyServices]);
380
246
 
381
- // Transfer code management functions (must be defined before deleteIdentityAndClearAccount)
382
- const getAllPendingTransfers = useCallback(() => {
383
- const pending: Array<{ transferId: string; data: TransferCodeData }> = [];
384
- transferCodesRef.current.forEach((data, transferId) => {
385
- if (data.state === 'pending') {
386
- pending.push({ transferId, data });
387
- }
388
- });
389
- return pending;
390
- }, []);
391
-
392
- const getActiveTransferId = useCallback(() => {
393
- return activeTransferIdRef.current;
394
- }, []);
395
-
396
- // Delete identity and clear all account data
397
- // In accounts app, deleting identity means losing the account completely
398
- const deleteIdentityAndClearAccount = useCallback(async (
399
- skipBackup: boolean = false,
400
- force: boolean = false,
401
- userConfirmed: boolean = false
402
- ): Promise<void> => {
403
- // CRITICAL: Check for active transfers before deletion (unless force is true)
404
- // This prevents accidental identity loss during transfer
405
- if (!force) {
406
- const pendingTransfers = getAllPendingTransfers();
407
- if (pendingTransfers.length > 0) {
408
- const activeTransferId = getActiveTransferId();
409
- const hasActiveTransfer = activeTransferId && pendingTransfers.some(t => t.transferId === activeTransferId);
410
-
411
- if (hasActiveTransfer) {
412
- throw new Error(
413
- 'Cannot delete identity: An active identity transfer is in progress. ' +
414
- 'Please wait for the transfer to complete or cancel it first. ' +
415
- 'If you proceed, you may lose access to your identity permanently.'
416
- );
417
- }
418
- }
419
- }
420
-
421
- // First, clear all account data
422
- await clearAllAccountData();
423
-
424
- // Then delete the identity keys
425
- await KeyManager.deleteIdentity(skipBackup, force, userConfirmed);
426
-
427
- // Clear identity cache for instant routing on next app launch
428
- clearIdentityCache();
429
- }, [clearAllAccountData, getAllPendingTransfers, getActiveTransferId]);
430
-
431
- // Network reconnect sync - TanStack Query automatically retries mutations on reconnect
432
- // We only need to sync identity if it's not synced
247
+ // Network state monitoring - updates authStore.isOnline
248
+ // When offline: isAuthenticated becomes false automatically
249
+ // When reconnect: isOnline set to true, allowing re-authentication
433
250
  useEffect(() => {
434
251
  if (!storage) return;
435
252
 
436
253
  let wasOffline = false;
437
254
  let checkTimeout: NodeJS.Timeout | null = null;
438
- let lastReconnectionLog = 0;
439
- const RECONNECTION_LOG_DEBOUNCE_MS = 5000; // 5 seconds
440
-
441
- // Circuit breaker and exponential backoff state
442
- const stateRef = {
443
- consecutiveFailures: 0,
444
- currentInterval: 10000, // Start with 10 seconds
445
- baseInterval: 10000, // Base interval in milliseconds
446
- maxInterval: 60000, // Maximum interval (60 seconds)
447
- maxFailures: 5, // Circuit breaker threshold
448
- };
449
255
 
450
256
  const scheduleNextCheck = () => {
451
257
  if (checkTimeout) {
452
258
  clearTimeout(checkTimeout);
453
259
  }
454
260
  checkTimeout = setTimeout(() => {
455
- checkNetworkAndSync();
456
- }, stateRef.currentInterval);
261
+ checkNetworkStatus();
262
+ }, 30000); // Check every 30 seconds
457
263
  };
458
264
 
459
- const checkNetworkAndSync = async () => {
265
+ const checkNetworkStatus = async () => {
460
266
  try {
461
267
  // Try a lightweight health check to see if we're online
462
- await oxyServices.healthCheck().catch(() => {
463
- wasOffline = true;
464
- throw new Error('Health check failed');
465
- });
268
+ await oxyServices.healthCheck();
466
269
 
467
- // Health check succeeded - reset circuit breaker and backoff
468
- if (stateRef.consecutiveFailures > 0) {
469
- stateRef.consecutiveFailures = 0;
470
- stateRef.currentInterval = stateRef.baseInterval;
471
- }
472
-
473
- // If we were offline and now we're online, sync identity if needed
270
+ // If we were offline and now we're online
474
271
  if (wasOffline) {
475
- const now = Date.now();
476
- const timeSinceLastLog = now - lastReconnectionLog;
477
-
478
- if (timeSinceLastLog >= RECONNECTION_LOG_DEBOUNCE_MS) {
479
- logger('Network reconnected, checking identity sync...');
480
- lastReconnectionLog = now;
481
-
482
- // Sync identity first (if not synced)
483
- try {
484
- const hasIdentityValue = await hasIdentity();
485
- if (hasIdentityValue) {
486
- // Check sync status directly - sync if not explicitly 'true'
487
- // undefined = not synced yet, 'false' = explicitly not synced, 'true' = synced
488
- const syncStatus = await storage.getItem('oxy_identity_synced');
489
- if (syncStatus !== 'true') {
490
- await syncIdentity();
491
- }
492
- }
493
- } catch (syncError: any) {
494
- // Skip sync silently if username is required (expected when offline onboarding)
495
- if (syncError?.code === 'USERNAME_REQUIRED' || syncError?.message === 'USERNAME_REQUIRED') {
496
- if (__DEV__) {
497
- loggerUtil.debug('Sync skipped - username required', { component: 'OxyContext', method: 'checkNetworkAndSync' }, syncError as unknown);
498
- }
499
- // Don't log or show error - username will be set later
500
- } else if (!isTimeoutOrNetworkError(syncError)) {
501
- // Only log unexpected errors - timeouts/network issues are expected when offline
502
- logger('Error syncing identity on reconnect', syncError);
503
- } else if (__DEV__) {
504
- loggerUtil.debug('Identity sync timeout (expected when offline)', { component: 'OxyContext', method: 'checkNetworkAndSync' }, syncError as unknown);
505
- }
506
- }
507
- }
508
-
272
+ logger('Network reconnected - setting online state');
273
+ setOnline(true);
509
274
  // TanStack Query will automatically retry pending mutations
510
- // Reset flag immediately after processing (whether logged or not)
275
+ // Session management will handle token refresh if needed
511
276
  wasOffline = false;
277
+ } else {
278
+ // We're online and were already online
279
+ setOnline(true);
512
280
  }
513
281
  } catch (error) {
514
282
  // Network check failed - we're offline
515
- wasOffline = true;
516
-
517
- // Increment failure count and apply exponential backoff
518
- stateRef.consecutiveFailures++;
519
-
520
- // Calculate new interval with exponential backoff, capped at maxInterval
521
- const backoffMultiplier = Math.min(
522
- Math.pow(2, stateRef.consecutiveFailures - 1),
523
- stateRef.maxInterval / stateRef.baseInterval
524
- );
525
- stateRef.currentInterval = Math.min(
526
- stateRef.baseInterval * backoffMultiplier,
527
- stateRef.maxInterval
528
- );
529
-
530
- // If we hit the circuit breaker threshold, use max interval
531
- if (stateRef.consecutiveFailures >= stateRef.maxFailures) {
532
- stateRef.currentInterval = stateRef.maxInterval;
283
+ if (!wasOffline) {
284
+ if (__DEV__) {
285
+ logger('Network appears offline - suspending authentication');
286
+ }
287
+ setOnline(false); // This will set isAuthenticated to false
533
288
  }
289
+ wasOffline = true;
534
290
  } finally {
535
- // Always schedule next check (will use updated interval)
536
291
  scheduleNextCheck();
537
292
  }
538
293
  };
539
294
 
540
295
  // Check immediately
541
- checkNetworkAndSync();
296
+ checkNetworkStatus();
542
297
 
543
298
  return () => {
544
299
  if (checkTimeout) {
545
300
  clearTimeout(checkTimeout);
546
301
  }
547
302
  };
548
- }, [oxyServices, storage, syncIdentity, logger]);
303
+ }, [oxyServices, storage, logger, setOnline]);
549
304
 
550
305
  const { getDeviceSessions, logoutAllDeviceSessions, updateDeviceName } = useDeviceManagement({
551
306
  oxyServices,
@@ -662,183 +417,6 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
662
417
  // Get userId from JWT token (MongoDB ObjectId) for socket room matching
663
418
  const userId = oxyServices.getCurrentUserId();
664
419
 
665
- // Transfer code storage interface
666
- interface TransferCodeData {
667
- code: string;
668
- sourceDeviceId: string | null;
669
- publicKey: string;
670
- timestamp: number;
671
- state: 'pending' | 'completed' | 'failed';
672
- }
673
-
674
- // Store transfer codes in memory for verification (also persisted to storage)
675
- // Map: transferId -> TransferCodeData
676
- const transferCodesRef = useRef<Map<string, TransferCodeData>>(new Map());
677
- const activeTransferIdRef = useRef<string | null>(null);
678
- const TRANSFER_CODES_STORAGE_KEY = `${storageKeyPrefix}_transfer_codes`;
679
- const ACTIVE_TRANSFER_STORAGE_KEY = `${storageKeyPrefix}_active_transfer_id`;
680
-
681
- // Clear stale transfer codes on startup
682
- // Transfers are ephemeral and should not persist across app restarts
683
- useEffect(() => {
684
- if (!storage || !isStorageReady) return;
685
-
686
- const clearStaleTransfers = async () => {
687
- try {
688
- // Clear any stored transfer codes - they're only valid during active transfer sessions
689
- await storage.removeItem(TRANSFER_CODES_STORAGE_KEY);
690
- await storage.removeItem(ACTIVE_TRANSFER_STORAGE_KEY);
691
-
692
- if (__DEV__) {
693
- logger('Cleared stale transfer codes on startup');
694
- }
695
- } catch (error) {
696
- if (__DEV__) {
697
- logger('Failed to clear stale transfer codes', error);
698
- }
699
- }
700
- };
701
-
702
- clearStaleTransfers();
703
- }, [storage, isStorageReady, logger, storageKeyPrefix]);
704
-
705
- // Persist transfer codes to storage whenever they change
706
- const persistTransferCodes = useCallback(async () => {
707
- if (!storage) return;
708
-
709
- try {
710
- const codesToStore: Record<string, TransferCodeData> = {};
711
- transferCodesRef.current.forEach((value, key) => {
712
- codesToStore[key] = value;
713
- });
714
- await storage.setItem(TRANSFER_CODES_STORAGE_KEY, JSON.stringify(codesToStore));
715
- } catch (error) {
716
- if (__DEV__) {
717
- logger('Failed to persist transfer codes', error);
718
- }
719
- }
720
- }, [storage, logger]);
721
-
722
- // Cleanup old transfer codes (older than 15 minutes)
723
- useEffect(() => {
724
- const cleanup = setInterval(async () => {
725
- const now = Date.now();
726
- const fifteenMinutes = 15 * 60 * 1000;
727
- let needsPersist = false;
728
-
729
- transferCodesRef.current.forEach((value, key) => {
730
- if (now - value.timestamp > fifteenMinutes) {
731
- transferCodesRef.current.delete(key);
732
- needsPersist = true;
733
- if (__DEV__) {
734
- logger('Cleaned up expired transfer code', { transferId: key });
735
- }
736
- }
737
- });
738
-
739
- // Clear active transfer if it was deleted
740
- if (activeTransferIdRef.current && !transferCodesRef.current.has(activeTransferIdRef.current)) {
741
- activeTransferIdRef.current = null;
742
- if (storage) {
743
- try {
744
- await storage.removeItem(ACTIVE_TRANSFER_STORAGE_KEY);
745
- } catch (error) {
746
- // Ignore storage errors
747
- }
748
- }
749
- }
750
-
751
- if (needsPersist) {
752
- await persistTransferCodes();
753
- }
754
- }, 60000); // Check every minute
755
-
756
- return () => clearInterval(cleanup);
757
- }, [logger, persistTransferCodes, storage]);
758
-
759
- // Transfer code management functions
760
- const storeTransferCode = useCallback(async (transferId: string, code: string, sourceDeviceId: string | null, publicKey: string) => {
761
- const transferData: TransferCodeData = {
762
- code,
763
- sourceDeviceId,
764
- publicKey,
765
- timestamp: Date.now(),
766
- state: 'pending',
767
- };
768
-
769
- transferCodesRef.current.set(transferId, transferData);
770
- activeTransferIdRef.current = transferId;
771
-
772
- // Persist to storage
773
- await persistTransferCodes();
774
- if (storage) {
775
- try {
776
- await storage.setItem(ACTIVE_TRANSFER_STORAGE_KEY, transferId);
777
- } catch (error) {
778
- if (__DEV__) {
779
- logger('Failed to persist active transfer ID', error);
780
- }
781
- }
782
- }
783
-
784
- if (__DEV__) {
785
- logger('Stored transfer code', { transferId, sourceDeviceId, publicKey: publicKey.substring(0, 16) + '...' });
786
- }
787
- }, [logger, persistTransferCodes, storage]);
788
-
789
- const getTransferCode = useCallback((transferId: string) => {
790
- return transferCodesRef.current.get(transferId) || null;
791
- }, []);
792
-
793
- const updateTransferState = useCallback(async (transferId: string, state: 'pending' | 'completed' | 'failed') => {
794
- const transferData = transferCodesRef.current.get(transferId);
795
- if (transferData) {
796
- transferData.state = state;
797
- transferCodesRef.current.set(transferId, transferData);
798
-
799
- // Clear active transfer if completed or failed
800
- if (state === 'completed' || state === 'failed') {
801
- if (activeTransferIdRef.current === transferId) {
802
- activeTransferIdRef.current = null;
803
- if (storage) {
804
- try {
805
- await storage.removeItem(ACTIVE_TRANSFER_STORAGE_KEY);
806
- } catch (error) {
807
- // Ignore storage errors
808
- }
809
- }
810
- }
811
- }
812
-
813
- await persistTransferCodes();
814
-
815
- if (__DEV__) {
816
- logger('Updated transfer state', { transferId, state });
817
- }
818
- }
819
- }, [logger, persistTransferCodes, storage]);
820
-
821
- const clearTransferCode = useCallback(async (transferId: string) => {
822
- transferCodesRef.current.delete(transferId);
823
-
824
- if (activeTransferIdRef.current === transferId) {
825
- activeTransferIdRef.current = null;
826
- if (storage) {
827
- try {
828
- await storage.removeItem(ACTIVE_TRANSFER_STORAGE_KEY);
829
- } catch (error) {
830
- // Ignore storage errors
831
- }
832
- }
833
- }
834
-
835
- await persistTransferCodes();
836
-
837
- if (__DEV__) {
838
- logger('Cleared transfer code', { transferId });
839
- }
840
- }, [logger, persistTransferCodes, storage]);
841
-
842
420
  const refreshSessionsWithUser = useCallback(
843
421
  () => refreshSessions(userId || undefined),
844
422
  [refreshSessions, userId],
@@ -856,154 +434,6 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
856
434
  logout().catch((remoteError) => logger('Failed to process remote sign out', remoteError));
857
435
  }, [logger, logout]);
858
436
 
859
- const handleIdentityTransferComplete = useCallback(
860
- async (data: { transferId: string; sourceDeviceId: string; publicKey: string; transferCode?: string; completedAt: string }) => {
861
- try {
862
- logger('Received identity transfer complete notification', {
863
- transferId: data.transferId,
864
- sourceDeviceId: data.sourceDeviceId,
865
- currentDeviceId,
866
- hasActiveSession: activeSessionId !== null,
867
- publicKey: data.publicKey.substring(0, 16) + '...',
868
- });
869
-
870
- const storedTransfer = getTransferCode(data.transferId);
871
-
872
- if (!storedTransfer) {
873
- logger('Transfer code not found for transferId', {
874
- transferId: data.transferId,
875
- });
876
- toast.error('Transfer verification failed: Code not found. Identity will not be deleted.');
877
- return;
878
- }
879
-
880
- // Verify publicKey matches first (most important check)
881
- const publicKeyMatches = data.publicKey === storedTransfer.publicKey;
882
- if (!publicKeyMatches) {
883
- logger('Public key mismatch for transfer', {
884
- transferId: data.transferId,
885
- receivedPublicKey: data.publicKey.substring(0, 16) + '...',
886
- storedPublicKey: storedTransfer.publicKey.substring(0, 16) + '...',
887
- });
888
- toast.error('Transfer verification failed: Public key mismatch. Identity will not be deleted.');
889
- return;
890
- }
891
-
892
- // Verify deviceId matches - very lenient since publicKey is the critical check
893
- // If publicKey matches, we allow deletion even if deviceId doesn't match exactly
894
- // This handles cases where deviceId might not be available or slightly different
895
- const deviceIdMatches =
896
- // Exact match
897
- (data.sourceDeviceId && data.sourceDeviceId === currentDeviceId) ||
898
- // Stored sourceDeviceId matches current deviceId
899
- (storedTransfer.sourceDeviceId && storedTransfer.sourceDeviceId === currentDeviceId);
900
-
901
- // If publicKey matches, we're very lenient with deviceId - only warn but don't block
902
- if (!deviceIdMatches && publicKeyMatches) {
903
- logger('Device ID mismatch for transfer, but publicKey matches - proceeding with deletion', {
904
- transferId: data.transferId,
905
- receivedDeviceId: data.sourceDeviceId,
906
- storedDeviceId: storedTransfer.sourceDeviceId,
907
- currentDeviceId,
908
- hasActiveSession: activeSessionId !== null,
909
- });
910
- // Proceed with deletion - publicKey match is the critical verification
911
- } else if (!deviceIdMatches && !publicKeyMatches) {
912
- // Both don't match - this is suspicious, block deletion
913
- logger('Device ID and publicKey mismatch for transfer', {
914
- transferId: data.transferId,
915
- receivedDeviceId: data.sourceDeviceId,
916
- currentDeviceId,
917
- });
918
- toast.error('Transfer verification failed: Device and key mismatch. Identity will not be deleted.');
919
- return;
920
- }
921
-
922
- // Verify transfer code matches (if provided)
923
- // Transfer code is optional - if not provided, we still proceed if publicKey matches
924
- if (data.transferCode) {
925
- const codeMatches = data.transferCode.toUpperCase() === storedTransfer.code.toUpperCase();
926
- if (!codeMatches) {
927
- logger('Transfer code mismatch, but publicKey matches - proceeding with deletion', {
928
- transferId: data.transferId,
929
- receivedCode: data.transferCode,
930
- storedCode: storedTransfer.code.substring(0, 2) + '****',
931
- });
932
- // Don't block - publicKey match is sufficient, code mismatch might be due to user error
933
- // Log warning but proceed
934
- }
935
- }
936
-
937
- // Check if transfer is too old (safety timeout - 10 minutes)
938
- const transferAge = Date.now() - storedTransfer.timestamp;
939
- const tenMinutes = 10 * 60 * 1000;
940
- if (transferAge > tenMinutes) {
941
- logger('Transfer confirmation received too late', {
942
- transferId: data.transferId,
943
- age: transferAge,
944
- ageMinutes: Math.round(transferAge / 60000),
945
- });
946
- toast.error('Transfer verification failed: Confirmation received too late. Identity will not be deleted.');
947
- clearTransferCode(data.transferId);
948
- return;
949
- }
950
-
951
- // NOTE: Target device verification already happened server-side when notifyTransferComplete was called
952
- // The server verified that the target device is authenticated and has the matching public key
953
- // Additional client-side verification is not necessary and would require source device authentication
954
- // which may not be available. The existing checks (public key match, transfer code, device ID) are sufficient.
955
-
956
- logger('All transfer verifications passed, deleting identity from source device', {
957
- transferId: data.transferId,
958
- sourceDeviceId: data.sourceDeviceId,
959
- publicKey: data.publicKey.substring(0, 16) + '...',
960
- });
961
-
962
- try {
963
- // Verify identity still exists before deletion (safety check)
964
- const identityStillExists = await KeyManager.hasIdentity();
965
- if (!identityStillExists) {
966
- logger('Identity already deleted - skipping deletion', {
967
- transferId: data.transferId,
968
- });
969
- await updateTransferState(data.transferId, 'completed');
970
- await clearTransferCode(data.transferId);
971
- return;
972
- }
973
-
974
- await deleteIdentityAndClearAccount(false, false, true);
975
-
976
- // Verify identity was actually deleted
977
- const identityDeleted = !(await KeyManager.hasIdentity());
978
- if (!identityDeleted) {
979
- logger('Identity deletion failed - identity still exists', {
980
- transferId: data.transferId,
981
- });
982
- await updateTransferState(data.transferId, 'failed');
983
- throw new Error('Identity deletion failed - identity still exists');
984
- }
985
-
986
- await updateTransferState(data.transferId, 'completed');
987
- await clearTransferCode(data.transferId);
988
-
989
- logger('Identity successfully deleted and transfer code cleared', {
990
- transferId: data.transferId,
991
- });
992
-
993
- toast.success('Identity successfully transferred and removed from this device');
994
- } catch (deleteError: any) {
995
- logger('Error during identity deletion', deleteError);
996
- await updateTransferState(data.transferId, 'failed');
997
- throw deleteError;
998
- }
999
- } catch (error: any) {
1000
- logger('Failed to delete identity after transfer', error);
1001
- toast.error(error?.message || 'Failed to remove identity from this device. Please try again manually from Security Settings.');
1002
- }
1003
- },
1004
- [deleteIdentityAndClearAccount, logger, getTransferCode, clearTransferCode, updateTransferState, currentDeviceId, activeSessionId, oxyServices],
1005
- );
1006
-
1007
437
  useSessionSocket({
1008
438
  userId,
1009
439
  activeSessionId,
@@ -1013,10 +443,8 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1013
443
  clearSessionState,
1014
444
  baseURL: oxyServices.getBaseURL(),
1015
445
  getAccessToken: () => oxyServices.getAccessToken(),
1016
- getTransferCode: getTransferCode,
1017
446
  onRemoteSignOut: handleRemoteSignOut,
1018
447
  onSessionRemoved: handleSessionRemoved,
1019
- onIdentityTransferComplete: handleIdentityTransferComplete,
1020
448
  });
1021
449
 
1022
450
  const switchSessionForContext = useCallback(
@@ -1058,7 +486,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1058
486
  oxyServices,
1059
487
  activeSessionId,
1060
488
  queryClient,
1061
- { syncIdentity, deviceId: currentDeviceId || undefined }
489
+ { deviceId: currentDeviceId || undefined }
1062
490
  );
1063
491
 
1064
492
  toast.success(translate(currentLanguage, 'editProfile.toasts.avatarUpdated') || 'Avatar updated');
@@ -1068,7 +496,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1068
496
  },
1069
497
  },
1070
498
  });
1071
- }, [oxyServices, currentLanguage, showBottomSheetForContext, activeSessionId, queryClient, syncIdentity]);
499
+ }, [oxyServices, currentLanguage, showBottomSheetForContext, activeSessionId, queryClient]);
1072
500
 
1073
501
  const contextValue: OxyContextState = useMemo(() => ({
1074
502
  user,
@@ -1084,24 +512,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1084
512
  currentLanguageMetadata,
1085
513
  currentLanguageName,
1086
514
  currentNativeLanguageName,
1087
- createIdentity: wrappedCreateIdentity,
1088
- importIdentity,
1089
- signIn,
1090
- hasIdentity,
1091
- getPublicKey,
1092
- isIdentitySynced,
1093
- syncIdentity,
1094
- deleteIdentityAndClearAccount,
1095
- storeTransferCode,
1096
- getTransferCode,
1097
- clearTransferCode,
1098
- getAllPendingTransfers,
1099
- getActiveTransferId,
1100
- updateTransferState,
1101
- identitySyncState: {
1102
- isSynced: isIdentitySyncedStore ?? true,
1103
- isSyncing: isSyncing ?? false,
1104
- },
515
+ completeSignIn,
1105
516
  logout,
1106
517
  logoutAll,
1107
518
  switchSession: switchSessionForContext,
@@ -1120,22 +531,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1120
531
  }), [
1121
532
  activeSessionId,
1122
533
  currentDeviceId,
1123
- wrappedCreateIdentity,
1124
- importIdentity,
1125
- signIn,
1126
- hasIdentity,
1127
- getPublicKey,
1128
- isIdentitySynced,
1129
- syncIdentity,
1130
- deleteIdentityAndClearAccount,
1131
- storeTransferCode,
1132
- getTransferCode,
1133
- clearTransferCode,
1134
- getAllPendingTransfers,
1135
- getActiveTransferId,
1136
- updateTransferState,
1137
- isIdentitySyncedStore,
1138
- isSyncing,
534
+ completeSignIn,
1139
535
  currentLanguage,
1140
536
  currentLanguageMetadata,
1141
537
  currentLanguageName,