@oxyhq/services 5.16.40 → 5.16.41

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 (267) hide show
  1. package/lib/commonjs/adapters/expo/crypto.js +56 -0
  2. package/lib/commonjs/adapters/expo/crypto.js.map +1 -0
  3. package/lib/commonjs/adapters/expo/fetch.js +30 -0
  4. package/lib/commonjs/adapters/expo/fetch.js.map +1 -0
  5. package/lib/commonjs/adapters/expo/index.js +48 -0
  6. package/lib/commonjs/adapters/expo/index.js.map +1 -0
  7. package/lib/commonjs/adapters/expo/storage.js +201 -0
  8. package/lib/commonjs/adapters/expo/storage.js.map +1 -0
  9. package/lib/commonjs/adapters/index.js +41 -0
  10. package/lib/commonjs/adapters/index.js.map +1 -0
  11. package/lib/commonjs/adapters/node/crypto.js +40 -0
  12. package/lib/commonjs/adapters/node/crypto.js.map +1 -0
  13. package/lib/commonjs/adapters/node/fetch.js +62 -0
  14. package/lib/commonjs/adapters/node/fetch.js.map +1 -0
  15. package/lib/commonjs/adapters/node/index.js +34 -0
  16. package/lib/commonjs/adapters/node/index.js.map +1 -0
  17. package/lib/commonjs/adapters/node/storage.js +163 -0
  18. package/lib/commonjs/adapters/node/storage.js.map +1 -0
  19. package/lib/commonjs/core/identity-session/DeviceManager.js +237 -0
  20. package/lib/commonjs/core/identity-session/DeviceManager.js.map +1 -0
  21. package/lib/commonjs/core/identity-session/INTEGRATION_GUIDE.md +287 -0
  22. package/lib/commonjs/core/identity-session/IdentityManager.js +400 -0
  23. package/lib/commonjs/core/identity-session/IdentityManager.js.map +1 -0
  24. package/lib/commonjs/core/identity-session/IdentitySessionCore.js +394 -0
  25. package/lib/commonjs/core/identity-session/IdentitySessionCore.js.map +1 -0
  26. package/lib/commonjs/core/identity-session/RefreshManager.js +137 -0
  27. package/lib/commonjs/core/identity-session/RefreshManager.js.map +1 -0
  28. package/lib/commonjs/core/identity-session/SessionManager.js +427 -0
  29. package/lib/commonjs/core/identity-session/SessionManager.js.map +1 -0
  30. package/lib/commonjs/core/identity-session/createIdentitySessionCore.js +24 -0
  31. package/lib/commonjs/core/identity-session/createIdentitySessionCore.js.map +1 -0
  32. package/lib/commonjs/core/identity-session/errors.js +176 -0
  33. package/lib/commonjs/core/identity-session/errors.js.map +1 -0
  34. package/lib/commonjs/core/identity-session/index.js +80 -0
  35. package/lib/commonjs/core/identity-session/index.js.map +1 -0
  36. package/lib/commonjs/core/identity-session/types.js +2 -0
  37. package/lib/commonjs/core/identity-session/types.js.map +1 -0
  38. package/lib/commonjs/core/index.js +2 -21
  39. package/lib/commonjs/core/index.js.map +1 -1
  40. package/lib/commonjs/index.js +58 -8
  41. package/lib/commonjs/index.js.map +1 -1
  42. package/lib/commonjs/models/interfaces.js +7 -0
  43. package/lib/commonjs/models/interfaces.js.map +1 -1
  44. package/lib/commonjs/ui/context/OxyContext.js +434 -820
  45. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  46. package/lib/commonjs/ui/hooks/useAvatarPicker.js +52 -0
  47. package/lib/commonjs/ui/hooks/useAvatarPicker.js.map +1 -0
  48. package/lib/commonjs/ui/hooks/useIdentityTransfer.js +125 -0
  49. package/lib/commonjs/ui/hooks/useIdentityTransfer.js.map +1 -0
  50. package/lib/commonjs/ui/hooks/useTransferCodesPersistence.js +81 -0
  51. package/lib/commonjs/ui/hooks/useTransferCodesPersistence.js.map +1 -0
  52. package/lib/commonjs/ui/screens/AccountCenterScreen.js +7 -2
  53. package/lib/commonjs/ui/screens/AccountCenterScreen.js.map +1 -1
  54. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +12 -5
  55. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  56. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +2 -2
  57. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -1
  58. package/lib/commonjs/ui/screens/ProfileScreen.js +6 -6
  59. package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
  60. package/lib/commonjs/ui/utils/sessionHelpers.js +7 -1
  61. package/lib/commonjs/ui/utils/sessionHelpers.js.map +1 -1
  62. package/lib/commonjs/utils/index.js +0 -7
  63. package/lib/commonjs/utils/index.js.map +1 -1
  64. package/lib/commonjs/utils/sessionUtils.js +8 -1
  65. package/lib/commonjs/utils/sessionUtils.js.map +1 -1
  66. package/lib/module/adapters/expo/crypto.js +51 -0
  67. package/lib/module/adapters/expo/crypto.js.map +1 -0
  68. package/lib/module/adapters/expo/fetch.js +26 -0
  69. package/lib/module/adapters/expo/fetch.js.map +1 -0
  70. package/lib/module/adapters/expo/index.js +45 -0
  71. package/lib/module/adapters/expo/index.js.map +1 -0
  72. package/lib/module/adapters/expo/storage.js +198 -0
  73. package/lib/module/adapters/expo/storage.js.map +1 -0
  74. package/lib/module/adapters/index.js +38 -0
  75. package/lib/module/adapters/index.js.map +1 -0
  76. package/lib/module/adapters/node/crypto.js +36 -0
  77. package/lib/module/adapters/node/crypto.js.map +1 -0
  78. package/lib/module/adapters/node/fetch.js +57 -0
  79. package/lib/module/adapters/node/fetch.js.map +1 -0
  80. package/lib/module/adapters/node/index.js +31 -0
  81. package/lib/module/adapters/node/index.js.map +1 -0
  82. package/lib/module/adapters/node/storage.js +159 -0
  83. package/lib/module/adapters/node/storage.js.map +1 -0
  84. package/lib/module/core/identity-session/DeviceManager.js +232 -0
  85. package/lib/module/core/identity-session/DeviceManager.js.map +1 -0
  86. package/lib/module/core/identity-session/INTEGRATION_GUIDE.md +287 -0
  87. package/lib/module/core/identity-session/IdentityManager.js +395 -0
  88. package/lib/module/core/identity-session/IdentityManager.js.map +1 -0
  89. package/lib/module/core/identity-session/IdentitySessionCore.js +390 -0
  90. package/lib/module/core/identity-session/IdentitySessionCore.js.map +1 -0
  91. package/lib/module/core/identity-session/RefreshManager.js +132 -0
  92. package/lib/module/core/identity-session/RefreshManager.js.map +1 -0
  93. package/lib/module/core/identity-session/SessionManager.js +422 -0
  94. package/lib/module/core/identity-session/SessionManager.js.map +1 -0
  95. package/lib/module/core/identity-session/createIdentitySessionCore.js +21 -0
  96. package/lib/module/core/identity-session/createIdentitySessionCore.js.map +1 -0
  97. package/lib/module/core/identity-session/errors.js +170 -0
  98. package/lib/module/core/identity-session/errors.js.map +1 -0
  99. package/lib/module/core/identity-session/index.js +17 -0
  100. package/lib/module/core/identity-session/index.js.map +1 -0
  101. package/lib/module/core/identity-session/types.js +2 -0
  102. package/lib/module/core/identity-session/types.js.map +1 -0
  103. package/lib/module/core/index.js +2 -3
  104. package/lib/module/core/index.js.map +1 -1
  105. package/lib/module/index.js +12 -2
  106. package/lib/module/index.js.map +1 -1
  107. package/lib/module/models/interfaces.js +7 -0
  108. package/lib/module/models/interfaces.js.map +1 -1
  109. package/lib/module/ui/context/OxyContext.js +436 -822
  110. package/lib/module/ui/context/OxyContext.js.map +1 -1
  111. package/lib/module/ui/hooks/useAvatarPicker.js +48 -0
  112. package/lib/module/ui/hooks/useAvatarPicker.js.map +1 -0
  113. package/lib/module/ui/hooks/useIdentityTransfer.js +121 -0
  114. package/lib/module/ui/hooks/useIdentityTransfer.js.map +1 -0
  115. package/lib/module/ui/hooks/useTransferCodesPersistence.js +77 -0
  116. package/lib/module/ui/hooks/useTransferCodesPersistence.js.map +1 -0
  117. package/lib/module/ui/screens/AccountCenterScreen.js +7 -2
  118. package/lib/module/ui/screens/AccountCenterScreen.js.map +1 -1
  119. package/lib/module/ui/screens/AccountSettingsScreen.js +12 -5
  120. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  121. package/lib/module/ui/screens/AccountSwitcherScreen.js +2 -2
  122. package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -1
  123. package/lib/module/ui/screens/ProfileScreen.js +6 -6
  124. package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
  125. package/lib/module/ui/utils/sessionHelpers.js +7 -1
  126. package/lib/module/ui/utils/sessionHelpers.js.map +1 -1
  127. package/lib/module/utils/index.js +2 -1
  128. package/lib/module/utils/index.js.map +1 -1
  129. package/lib/module/utils/sessionUtils.js +8 -1
  130. package/lib/module/utils/sessionUtils.js.map +1 -1
  131. package/lib/typescript/adapters/expo/crypto.d.ts +17 -0
  132. package/lib/typescript/adapters/expo/crypto.d.ts.map +1 -0
  133. package/lib/typescript/adapters/expo/fetch.d.ts +16 -0
  134. package/lib/typescript/adapters/expo/fetch.d.ts.map +1 -0
  135. package/lib/typescript/adapters/expo/index.d.ts +23 -0
  136. package/lib/typescript/adapters/expo/index.d.ts.map +1 -0
  137. package/lib/typescript/adapters/expo/storage.d.ts +23 -0
  138. package/lib/typescript/adapters/expo/storage.d.ts.map +1 -0
  139. package/lib/typescript/adapters/index.d.ts +13 -0
  140. package/lib/typescript/adapters/index.d.ts.map +1 -0
  141. package/lib/typescript/adapters/node/crypto.d.ts +17 -0
  142. package/lib/typescript/adapters/node/crypto.d.ts.map +1 -0
  143. package/lib/typescript/adapters/node/fetch.d.ts +16 -0
  144. package/lib/typescript/adapters/node/fetch.d.ts.map +1 -0
  145. package/lib/typescript/adapters/node/index.d.ts +23 -0
  146. package/lib/typescript/adapters/node/index.d.ts.map +1 -0
  147. package/lib/typescript/adapters/node/storage.d.ts +23 -0
  148. package/lib/typescript/adapters/node/storage.d.ts.map +1 -0
  149. package/lib/typescript/core/identity-session/DeviceManager.d.ts +64 -0
  150. package/lib/typescript/core/identity-session/DeviceManager.d.ts.map +1 -0
  151. package/lib/typescript/core/identity-session/IdentityManager.d.ts +88 -0
  152. package/lib/typescript/core/identity-session/IdentityManager.d.ts.map +1 -0
  153. package/lib/typescript/core/identity-session/IdentitySessionCore.d.ts +141 -0
  154. package/lib/typescript/core/identity-session/IdentitySessionCore.d.ts.map +1 -0
  155. package/lib/typescript/core/identity-session/RefreshManager.d.ts +36 -0
  156. package/lib/typescript/core/identity-session/RefreshManager.d.ts.map +1 -0
  157. package/lib/typescript/core/identity-session/SessionManager.d.ts +104 -0
  158. package/lib/typescript/core/identity-session/SessionManager.d.ts.map +1 -0
  159. package/lib/typescript/core/identity-session/createIdentitySessionCore.d.ts +11 -0
  160. package/lib/typescript/core/identity-session/createIdentitySessionCore.d.ts.map +1 -0
  161. package/lib/typescript/core/identity-session/errors.d.ts +63 -0
  162. package/lib/typescript/core/identity-session/errors.d.ts.map +1 -0
  163. package/lib/typescript/core/identity-session/index.d.ts +14 -0
  164. package/lib/typescript/core/identity-session/index.d.ts.map +1 -0
  165. package/lib/typescript/core/identity-session/types.d.ts +196 -0
  166. package/lib/typescript/core/identity-session/types.d.ts.map +1 -0
  167. package/lib/typescript/core/index.d.ts +1 -3
  168. package/lib/typescript/core/index.d.ts.map +1 -1
  169. package/lib/typescript/core/mixins/index.d.ts +2 -2
  170. package/lib/typescript/index.d.ts +3 -2
  171. package/lib/typescript/index.d.ts.map +1 -1
  172. package/lib/typescript/models/interfaces.d.ts +5 -36
  173. package/lib/typescript/models/interfaces.d.ts.map +1 -1
  174. package/lib/typescript/models/session.d.ts +3 -16
  175. package/lib/typescript/models/session.d.ts.map +1 -1
  176. package/lib/typescript/ui/context/OxyContext.d.ts +2 -25
  177. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  178. package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts +7 -8
  179. package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
  180. package/lib/typescript/ui/hooks/mutations/useServicesMutations.d.ts +1 -1
  181. package/lib/typescript/ui/hooks/mutations/useServicesMutations.d.ts.map +1 -1
  182. package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts +5 -5
  183. package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
  184. package/lib/typescript/ui/hooks/useAvatarPicker.d.ts +18 -0
  185. package/lib/typescript/ui/hooks/useAvatarPicker.d.ts.map +1 -0
  186. package/lib/typescript/ui/hooks/useIdentityTransfer.d.ts +24 -0
  187. package/lib/typescript/ui/hooks/useIdentityTransfer.d.ts.map +1 -0
  188. package/lib/typescript/ui/hooks/useTransferCodesPersistence.d.ts +6 -0
  189. package/lib/typescript/ui/hooks/useTransferCodesPersistence.d.ts.map +1 -0
  190. package/lib/typescript/ui/screens/AccountCenterScreen.d.ts.map +1 -1
  191. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  192. package/lib/typescript/ui/utils/sessionHelpers.d.ts +1 -0
  193. package/lib/typescript/ui/utils/sessionHelpers.d.ts.map +1 -1
  194. package/lib/typescript/utils/index.d.ts +0 -2
  195. package/lib/typescript/utils/index.d.ts.map +1 -1
  196. package/lib/typescript/utils/sessionUtils.d.ts.map +1 -1
  197. package/package.json +1 -1
  198. package/src/adapters/expo/crypto.ts +55 -0
  199. package/src/adapters/expo/fetch.ts +28 -0
  200. package/src/adapters/expo/index.ts +51 -0
  201. package/src/adapters/expo/storage.ts +228 -0
  202. package/src/adapters/index.ts +40 -0
  203. package/src/adapters/node/crypto.ts +39 -0
  204. package/src/adapters/node/fetch.ts +59 -0
  205. package/src/adapters/node/index.ts +37 -0
  206. package/src/adapters/node/storage.ts +170 -0
  207. package/src/core/identity-session/DeviceManager.ts +273 -0
  208. package/src/core/identity-session/INTEGRATION_GUIDE.md +287 -0
  209. package/src/core/identity-session/IdentityManager.ts +474 -0
  210. package/src/core/identity-session/IdentitySessionCore.ts +464 -0
  211. package/src/core/identity-session/RefreshManager.ts +189 -0
  212. package/src/core/identity-session/SessionManager.ts +500 -0
  213. package/src/core/identity-session/createIdentitySessionCore.ts +19 -0
  214. package/src/core/identity-session/errors.ts +197 -0
  215. package/src/core/identity-session/index.ts +15 -0
  216. package/src/core/identity-session/types.ts +188 -0
  217. package/src/core/index.ts +3 -4
  218. package/src/index.ts +28 -3
  219. package/src/models/interfaces.ts +12 -39
  220. package/src/models/session.ts +6 -16
  221. package/src/ui/context/OxyContext.tsx +442 -871
  222. package/src/ui/hooks/auth/index.ts +1 -0
  223. package/src/ui/hooks/useAvatarPicker.ts +62 -0
  224. package/src/ui/hooks/useIdentityTransfer.ts +135 -0
  225. package/src/ui/hooks/useTransferCodesPersistence.ts +80 -0
  226. package/src/ui/screens/AccountCenterScreen.tsx +7 -2
  227. package/src/ui/screens/AccountSettingsScreen.tsx +15 -8
  228. package/src/ui/screens/AccountSwitcherScreen.tsx +2 -2
  229. package/src/ui/screens/ProfileScreen.tsx +10 -10
  230. package/src/ui/utils/sessionHelpers.ts +7 -0
  231. package/src/utils/index.ts +1 -2
  232. package/src/utils/sessionUtils.ts +8 -0
  233. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +0 -732
  234. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +0 -1
  235. package/lib/commonjs/ui/context/hooks/useDeviceManagement.js +0 -73
  236. package/lib/commonjs/ui/context/hooks/useDeviceManagement.js.map +0 -1
  237. package/lib/commonjs/ui/hooks/useDeviceManagement.js +0 -73
  238. package/lib/commonjs/ui/hooks/useDeviceManagement.js.map +0 -1
  239. package/lib/commonjs/ui/hooks/useSessionManagement.js +0 -281
  240. package/lib/commonjs/ui/hooks/useSessionManagement.js.map +0 -1
  241. package/lib/commonjs/utils/deviceManager.js +0 -177
  242. package/lib/commonjs/utils/deviceManager.js.map +0 -1
  243. package/lib/module/ui/context/hooks/useAuthOperations.js +0 -726
  244. package/lib/module/ui/context/hooks/useAuthOperations.js.map +0 -1
  245. package/lib/module/ui/context/hooks/useDeviceManagement.js +0 -68
  246. package/lib/module/ui/context/hooks/useDeviceManagement.js.map +0 -1
  247. package/lib/module/ui/hooks/useDeviceManagement.js +0 -68
  248. package/lib/module/ui/hooks/useDeviceManagement.js.map +0 -1
  249. package/lib/module/ui/hooks/useSessionManagement.js +0 -276
  250. package/lib/module/ui/hooks/useSessionManagement.js.map +0 -1
  251. package/lib/module/utils/deviceManager.js +0 -171
  252. package/lib/module/utils/deviceManager.js.map +0 -1
  253. package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts +0 -59
  254. package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts.map +0 -1
  255. package/lib/typescript/ui/context/hooks/useDeviceManagement.d.ts +0 -27
  256. package/lib/typescript/ui/context/hooks/useDeviceManagement.d.ts.map +0 -1
  257. package/lib/typescript/ui/hooks/useDeviceManagement.d.ts +0 -27
  258. package/lib/typescript/ui/hooks/useDeviceManagement.d.ts.map +0 -1
  259. package/lib/typescript/ui/hooks/useSessionManagement.d.ts +0 -41
  260. package/lib/typescript/ui/hooks/useSessionManagement.d.ts.map +0 -1
  261. package/lib/typescript/utils/deviceManager.d.ts +0 -66
  262. package/lib/typescript/utils/deviceManager.d.ts.map +0 -1
  263. package/src/ui/context/hooks/useAuthOperations.ts +0 -801
  264. package/src/ui/context/hooks/useDeviceManagement.ts +0 -108
  265. package/src/ui/hooks/useDeviceManagement.ts +0 -108
  266. package/src/ui/hooks/useSessionManagement.ts +0 -401
  267. package/src/utils/deviceManager.ts +0 -198
@@ -4,7 +4,6 @@ import {
4
4
  useCallback,
5
5
  useContext,
6
6
  useEffect,
7
- useLayoutEffect,
8
7
  useMemo,
9
8
  useRef,
10
9
  useState,
@@ -18,25 +17,22 @@ import { toast } from '../../lib/sonner';
18
17
  import { useAuthStore, type AuthState } from '../stores/authStore';
19
18
  import { useShallow } from 'zustand/react/shallow';
20
19
  import { useSessionSocket } from '../hooks/useSessionSocket';
21
- import type { UseFollowHook } from '../hooks/useFollow.types';
22
20
  import { useStorage } from '../hooks/useStorage';
23
21
  import { useLanguageManagement } from '../hooks/useLanguageManagement';
24
- import { useSessionManagement } from '../hooks/useSessionManagement';
25
- import { useAuthOperations } from './hooks/useAuthOperations';
26
- import { useDeviceManagement } from '../hooks/useDeviceManagement';
27
22
  import { getStorageKeys } from '../utils/storageHelpers';
28
23
  import { isInvalidSessionError, isTimeoutOrNetworkError } from '../utils/errorHandlers';
29
24
  import type { RouteName } from '../navigation/routes';
30
25
  import { showBottomSheet as globalShowBottomSheet } from '../navigation/bottomSheetManager';
31
26
  import { useQueryClient } from '@tanstack/react-query';
32
27
  import { clearQueryCache } from '../hooks/queryClient';
33
- import { KeyManager, type BackupData } from '../../crypto';
28
+ import { createIdentitySessionCore, type IdentitySessionCore, type BackupData } from '../../core/identity-session';
34
29
  import { translate } from '../../i18n';
35
- import { updateAvatarVisibility, updateProfileWithAvatar } from '../utils/avatarUtils';
36
30
  import { useAccountStore } from '../stores/accountStore';
37
- import { logger as loggerUtil } from '../../utils/loggerUtils';
38
- import { useTransferStore, useTransferCodesForPersistence } from '../stores/transferStore';
31
+ import { useTransferStore } from '../stores/transferStore';
39
32
  import { useCheckPendingTransfers } from '../hooks/useTransferQueries';
33
+ import { useTransferCodesPersistence } from '../hooks/useTransferCodesPersistence';
34
+ import { useIdentityTransfer } from '../hooks/useIdentityTransfer';
35
+ import { useAvatarPicker } from '../hooks/useAvatarPicker';
40
36
 
41
37
  export interface OxyContextState {
42
38
  user: User | null;
@@ -62,12 +58,6 @@ export interface OxyContextState {
62
58
  isIdentitySynced: () => Promise<boolean>;
63
59
  syncIdentity: (username?: string) => Promise<User>;
64
60
  deleteIdentityAndClearAccount: (skipBackup?: boolean, force?: boolean, userConfirmed?: boolean) => Promise<void>;
65
- storeTransferCode: (transferId: string, code: string, sourceDeviceId: string | null, publicKey: string) => Promise<void>;
66
- getTransferCode: (transferId: string) => { code: string; sourceDeviceId: string | null; publicKey: string; timestamp: number; state: 'pending' | 'completed' | 'failed' } | null;
67
- clearTransferCode: (transferId: string) => Promise<void>;
68
- getAllPendingTransfers: () => Array<{ transferId: string; data: { code: string; sourceDeviceId: string | null; publicKey: string; timestamp: number; state: 'pending' | 'completed' | 'failed' } }>;
69
- getActiveTransferId: () => string | null;
70
- updateTransferState: (transferId: string, state: 'pending' | 'completed' | 'failed') => Promise<void>;
71
61
 
72
62
  // Identity sync state (reactive, from Zustand store)
73
63
  identitySyncState: {
@@ -78,7 +68,7 @@ export interface OxyContextState {
78
68
  // Session management
79
69
  logout: (targetSessionId?: string) => Promise<void>;
80
70
  logoutAll: () => Promise<void>;
81
- switchSession: (sessionId: string) => Promise<void>;
71
+ switchSession: (sessionId: string) => Promise<User>;
82
72
  removeSession: (sessionId: string) => Promise<void>;
83
73
  refreshSessions: () => Promise<void>;
84
74
  setLanguage: (languageId: string) => Promise<void>;
@@ -96,7 +86,6 @@ export interface OxyContextState {
96
86
  clearSessionState: () => Promise<void>;
97
87
  clearAllAccountData: () => Promise<void>;
98
88
  oxyServices: OxyServices;
99
- useFollow?: UseFollowHook;
100
89
  showBottomSheet?: (screenOrConfig: RouteName | { screen: RouteName; props?: Record<string, unknown> }) => void;
101
90
  openAvatarPicker: () => void;
102
91
  }
@@ -112,35 +101,6 @@ export interface OxyContextProviderProps {
112
101
  onError?: (error: ApiError) => void;
113
102
  }
114
103
 
115
- let cachedUseFollowHook: UseFollowHook | null = null;
116
-
117
- const loadUseFollowHook = (): UseFollowHook => {
118
- if (cachedUseFollowHook) {
119
- return cachedUseFollowHook;
120
- }
121
-
122
- try {
123
- // eslint-disable-next-line @typescript-eslint/no-var-requires
124
- const { useFollow } = require('../hooks/useFollow');
125
- cachedUseFollowHook = useFollow as UseFollowHook;
126
- return cachedUseFollowHook;
127
- } catch (error) {
128
- if (__DEV__) {
129
- loggerUtil.warn(
130
- 'useFollow hook is not available. Please import useFollow from @oxyhq/services directly.',
131
- { component: 'OxyContext', method: 'loadUseFollowHook' },
132
- error
133
- );
134
- }
135
-
136
- const fallback: UseFollowHook = () => {
137
- throw new Error('useFollow hook is only available in the UI bundle. Import it from @oxyhq/services.');
138
- };
139
-
140
- cachedUseFollowHook = fallback;
141
- return cachedUseFollowHook;
142
- }
143
- };
144
104
 
145
105
  export const OxyProvider: React.FC<OxyContextProviderProps> = ({
146
106
  children,
@@ -195,7 +155,6 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
195
155
  );
196
156
 
197
157
  const [tokenReady, setTokenReady] = useState(true);
198
- const initializedRef = useRef(false);
199
158
  const setAuthState = useAuthStore.setState;
200
159
 
201
160
  const logger = useCallback((message: string, err?: unknown) => {
@@ -212,59 +171,30 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
212
171
 
213
172
  const { storage, isReady: isStorageReady } = useStorage({ onError, logger });
214
173
 
215
- // Invalidate KeyManager cache on mount to prevent stale cache issues
216
- // useLayoutEffect runs synchronously after render but before paint, ensuring cache
217
- // invalidation happens before any async operations start
218
- useLayoutEffect(() => {
219
- if (Platform.OS !== 'web') {
220
- KeyManager.invalidateCache();
221
- }
222
- }, []);
174
+ // Initialize Identity Session Core
175
+ const [identityCore, setIdentityCore] = useState<IdentitySessionCore | null>(null);
176
+ const [isCoreInitialized, setIsCoreInitialized] = useState(false);
223
177
 
224
- // Identity integrity check and auto-restore on startup
225
- // Skip on web platform - identity storage is only available on native platforms
226
178
  useEffect(() => {
227
- if (!storage || !isStorageReady) return;
228
- if (Platform.OS === 'web') return; // Identity operations are native-only
229
-
230
- const checkAndRestoreIdentity = async () => {
179
+ let mounted = true;
180
+ const initCore = async () => {
231
181
  try {
232
- // Cache was already invalidated synchronously above
233
-
234
- // Check if identity exists and verify integrity
235
- const hasIdentity = await KeyManager.hasIdentity();
236
- if (hasIdentity) {
237
- const isValid = await KeyManager.verifyIdentityIntegrity();
238
- if (!isValid) {
239
- // Try to restore from backup (cache will be invalidated inside restoreIdentityFromBackup)
240
- const restored = await KeyManager.restoreIdentityFromBackup();
241
- if (__DEV__) {
242
- logger(restored
243
- ? 'Identity restored from backup successfully'
244
- : 'Identity integrity check failed - user may need to restore from backup file'
245
- );
246
- }
247
- } else {
248
- // Identity is valid - ensure backup is up to date
249
- await KeyManager.backupIdentity();
250
- }
251
- } else {
252
- // No identity - try to restore from backup (cache will be invalidated inside restoreIdentityFromBackup)
253
- const restored = await KeyManager.restoreIdentityFromBackup();
254
- if (restored && __DEV__) {
255
- logger('Identity restored from backup on startup');
256
- }
182
+ const core = await createIdentitySessionCore(oxyServices.getBaseURL() || baseURL);
183
+ if (mounted) {
184
+ setIdentityCore(core);
185
+ setIsCoreInitialized(true);
257
186
  }
258
187
  } catch (error) {
259
188
  if (__DEV__) {
260
- logger('Error during identity integrity check', error);
189
+ logger('Failed to initialize identity session core', error);
261
190
  }
262
- // Don't block app startup - user can recover with backup file
263
191
  }
264
192
  };
265
-
266
- checkAndRestoreIdentity();
267
- }, [storage, isStorageReady, logger]);
193
+ initCore();
194
+ return () => {
195
+ mounted = false;
196
+ };
197
+ }, [oxyServices, baseURL, logger]);
268
198
 
269
199
  const {
270
200
  currentLanguage,
@@ -282,70 +212,115 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
282
212
 
283
213
  const queryClient = useQueryClient();
284
214
 
285
- const {
286
- sessions,
287
- activeSessionId,
288
- setActiveSessionId: setActiveSessionIdFromHook,
289
- updateSessions,
290
- switchSession,
291
- refreshSessions,
292
- clearSessionState,
293
- saveActiveSessionId,
294
- trackRemovedSession,
295
- } = useSessionManagement({
296
- oxyServices,
297
- storage,
298
- storageKeyPrefix,
299
- loginSuccess,
300
- logoutStore,
301
- applyLanguagePreference,
302
- onAuthStateChange,
303
- onError,
304
- setAuthError: (message) => setAuthState({ error: message }),
305
- logger,
306
- setTokenReady,
307
- queryClient,
308
- });
215
+ // Session state management using core
216
+ const [sessions, setSessions] = useState<ClientSession[]>([]);
217
+ const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
309
218
 
310
- const {
311
- createIdentity,
312
- importIdentity: importIdentityBase,
313
- signIn,
314
- logout,
315
- logoutAll,
316
- hasIdentity,
317
- getPublicKey,
318
- isIdentitySynced,
319
- syncIdentity: syncIdentityBase,
320
- } = useAuthOperations({
321
- oxyServices,
322
- storage,
323
- sessions,
324
- activeSessionId,
325
- setActiveSessionId: setActiveSessionIdFromHook,
326
- updateSessions,
327
- saveActiveSessionId,
328
- clearSessionState,
329
- switchSession,
330
- applyLanguagePreference,
331
- onAuthStateChange,
332
- onError,
333
- loginSuccess,
334
- loginFailure,
335
- logoutStore,
336
- setAuthState,
337
- setIdentitySynced,
338
- setSyncing,
339
- logger,
340
- });
219
+ // Load sessions from core
220
+ const loadSessionsFromCore = useCallback(async () => {
221
+ if (!identityCore || !isCoreInitialized) return;
222
+ try {
223
+ const coreSessions = await identityCore.getAllSessions();
224
+ const activeId = await identityCore.getActiveSessionId();
225
+
226
+ // Convert core sessions to ClientSession format
227
+ const clientSessions: ClientSession[] = coreSessions.map(s => ({
228
+ deviceInfo: s.deviceInfo,
229
+ sessionId: s.sessionId,
230
+ deviceId: s.deviceId,
231
+ userId: s.userId,
232
+ expiresAt: s.expiresAt,
233
+ lastActive: s.lastActive,
234
+ isCurrent: s.isCurrent || false,
235
+ }));
236
+
237
+ setSessions(clientSessions);
238
+ setActiveSessionId(activeId);
239
+ } catch (error) {
240
+ if (__DEV__) {
241
+ logger('Failed to load sessions from core', error);
242
+ }
243
+ }
244
+ }, [identityCore, isCoreInitialized, logger]);
245
+
246
+ // Load sessions when core is initialized
247
+ useEffect(() => {
248
+ if (isCoreInitialized) {
249
+ loadSessionsFromCore();
250
+ }
251
+ }, [isCoreInitialized, loadSessionsFromCore]);
252
+
253
+ // Subscribe to core events
254
+ useEffect(() => {
255
+ if (!identityCore || !isCoreInitialized) return;
256
+
257
+ const unsubscribe = identityCore.subscribe((event) => {
258
+ if (event.type === 'session:created' || event.type === 'session:refreshed' || event.type === 'session:invalidated' || event.type === 'session:all-invalidated') {
259
+ loadSessionsFromCore();
260
+ }
261
+ });
262
+
263
+ return unsubscribe;
264
+ }, [identityCore, isCoreInitialized, loadSessionsFromCore]);
265
+
266
+ // Identity operations using core
267
+ const hasIdentity = useCallback(async (): Promise<boolean> => {
268
+ if (!identityCore || !isCoreInitialized) return false;
269
+ return await identityCore.hasIdentity();
270
+ }, [identityCore, isCoreInitialized]);
271
+
272
+ const getPublicKey = useCallback(async (): Promise<string | null> => {
273
+ if (!identityCore || !isCoreInitialized) return null;
274
+ return await identityCore.getPublicKey();
275
+ }, [identityCore, isCoreInitialized]);
276
+
277
+ const isIdentitySynced = useCallback(async (): Promise<boolean> => {
278
+ if (!storage) return false;
279
+ const synced = await storage.getItem('oxy_identity_synced');
280
+ return synced === 'true';
281
+ }, [storage]);
282
+
283
+ const createIdentity = useCallback(async (username?: string): Promise<{ synced: boolean }> => {
284
+ if (!identityCore || !isCoreInitialized) {
285
+ throw new Error('Identity core not initialized');
286
+ }
341
287
 
342
- // syncIdentity - TanStack Query handles offline mutations automatically
343
- const syncIdentity = useCallback(() => syncIdentityBase(), [syncIdentityBase]);
288
+ setSyncing(true);
289
+ try {
290
+ // Create identity using core
291
+ const identity = await identityCore.createIdentity(username);
292
+
293
+ // Try to register with backend
294
+ let synced = false;
295
+ try {
296
+ const { signature, publicKey, timestamp } = await identityCore.createRegistrationSignature();
297
+ const result = await oxyServices.register(publicKey, signature, timestamp, username);
298
+ if (result?.user) {
299
+ loginSuccess(result.user);
300
+ synced = true;
301
+ await storage?.setItem('oxy_identity_synced', 'true');
302
+ setIdentitySynced(true);
303
+ }
304
+ } catch (error) {
305
+ // Registration failed (likely offline) - identity still created locally
306
+ if (__DEV__) {
307
+ logger('Failed to register identity with backend (offline mode)', error);
308
+ }
309
+ }
310
+
311
+ return { synced };
312
+ } finally {
313
+ setSyncing(false);
314
+ }
315
+ }, [identityCore, isCoreInitialized, oxyServices, storage, loginSuccess, setIdentitySynced, setSyncing, logger]);
344
316
 
345
- // Wrapper for importIdentity to handle legacy calls gracefully
346
317
  const importIdentity = useCallback(
347
318
  async (backupData: BackupData | string, password?: string): Promise<{ synced: boolean }> => {
348
- // Handle legacy calls with single string argument (old recovery phrase signature)
319
+ if (!identityCore || !isCoreInitialized) {
320
+ throw new Error('Identity core not initialized');
321
+ }
322
+
323
+ // Handle legacy calls with single string argument
349
324
  if (typeof backupData === 'string') {
350
325
  throw new Error('Recovery phrase import is no longer supported. Please use backup file import or QR code transfer instead.');
351
326
  }
@@ -355,94 +330,290 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
355
330
  throw new Error('Password is required for backup file import.');
356
331
  }
357
332
 
358
- return importIdentityBase(backupData, password);
333
+ setSyncing(true);
334
+ try {
335
+ // Import identity using core
336
+ const identity = await identityCore.importIdentity(backupData, password);
337
+
338
+ // Try to register with backend if not already registered
339
+ let synced = false;
340
+ try {
341
+ const { signature, publicKey, timestamp } = await identityCore.createRegistrationSignature();
342
+ const result = await oxyServices.register(publicKey, signature, timestamp);
343
+ if (result?.user) {
344
+ loginSuccess(result.user);
345
+ synced = true;
346
+ await storage?.setItem('oxy_identity_synced', 'true');
347
+ setIdentitySynced(true);
348
+ }
349
+ } catch (error: any) {
350
+ // Check if user already exists (409 conflict)
351
+ if (error?.status === 409) {
352
+ // User already registered - try to sign in instead
353
+ try {
354
+ await signIn();
355
+ synced = true;
356
+ await storage?.setItem('oxy_identity_synced', 'true');
357
+ setIdentitySynced(true);
358
+ } catch (signInError) {
359
+ if (__DEV__) {
360
+ logger('Failed to sign in after import', signInError);
361
+ }
362
+ }
363
+ } else if (__DEV__) {
364
+ logger('Failed to register identity with backend (offline mode)', error);
365
+ }
366
+ }
367
+
368
+ return { synced };
369
+ } finally {
370
+ setSyncing(false);
371
+ }
359
372
  },
360
- [importIdentityBase]
373
+ [identityCore, isCoreInitialized, oxyServices, storage, loginSuccess, setIdentitySynced, setSyncing, logger]
361
374
  );
362
375
 
363
- // Storage keys for transfer codes
364
- const TRANSFER_CODES_STORAGE_KEY = `${storageKeyPrefix}_transfer_codes`;
365
- const ACTIVE_TRANSFER_STORAGE_KEY = `${storageKeyPrefix}_active_transfer_id`;
376
+ const syncIdentity = useCallback(async (username?: string): Promise<User> => {
377
+ if (!identityCore || !isCoreInitialized) {
378
+ throw new Error('Identity core not initialized');
379
+ }
380
+
381
+ const publicKey = await identityCore.getPublicKey();
382
+ if (!publicKey) {
383
+ throw new Error('No identity found');
384
+ }
366
385
 
367
- // Clear all account data when identity is lost (for accounts app)
368
- // In accounts app, identity = account, so losing identity means losing everything
369
- const clearAllAccountData = useCallback(async (): Promise<void> => {
370
- if (__DEV__) {
371
- logger('Clearing all account data - identity changed or lost');
386
+ setSyncing(true);
387
+ try {
388
+ const { signature, publicKey: pk, timestamp } = await identityCore.createRegistrationSignature();
389
+ const result = await oxyServices.register(pk, signature, timestamp, username);
390
+ if (result?.user) {
391
+ loginSuccess(result.user);
392
+ await storage?.setItem('oxy_identity_synced', 'true');
393
+ setIdentitySynced(true);
394
+ return result.user;
395
+ }
396
+ throw new Error('Registration failed');
397
+ } catch (error: any) {
398
+ // Check if user already exists (409 conflict) - try to sign in
399
+ if (error?.status === 409) {
400
+ const user = await signIn();
401
+ await storage?.setItem('oxy_identity_synced', 'true');
402
+ setIdentitySynced(true);
403
+ return user;
404
+ }
405
+ throw error;
406
+ } finally {
407
+ setSyncing(false);
408
+ }
409
+ }, [identityCore, isCoreInitialized, oxyServices, storage, loginSuccess, setIdentitySynced, setSyncing]);
410
+
411
+ const signIn = useCallback(async (deviceName?: string): Promise<User> => {
412
+ if (!identityCore || !isCoreInitialized) {
413
+ throw new Error('Identity core not initialized');
414
+ }
415
+
416
+ setAuthState({ isLoading: true });
417
+ try {
418
+ // Create session using core
419
+ const session = await identityCore.createSession(deviceName);
420
+
421
+ // Get user from OxyServices
422
+ const user = await oxyServices.getUserBySession(session.sessionId);
423
+ if (!user) {
424
+ throw new Error('Failed to get user data');
425
+ }
426
+
427
+ // Set token in OxyServices
428
+ if (session.accessToken) {
429
+ oxyServices.setTokens(session.accessToken, session.refreshToken || undefined);
430
+ }
431
+
432
+ // Update state
433
+ loginSuccess(user);
434
+ await loadSessionsFromCore();
435
+ onAuthStateChange?.(user);
436
+ await applyLanguagePreference(user);
437
+ setTokenReady(true);
438
+
439
+ return user;
440
+ } catch (error) {
441
+ const errorMessage = error instanceof Error ? error.message : String(error);
442
+ loginFailure(errorMessage);
443
+ throw error;
444
+ } finally {
445
+ setAuthState({ isLoading: false });
372
446
  }
447
+ }, [identityCore, isCoreInitialized, oxyServices, loginSuccess, loginFailure, setAuthState, loadSessionsFromCore, onAuthStateChange, applyLanguagePreference, setTokenReady]);
448
+
449
+ // Session management functions
450
+ const clearSessionState = useCallback(async (): Promise<void> => {
451
+ setSessions([]);
452
+ setActiveSessionId(null);
453
+ logoutStore();
454
+ onAuthStateChange?.(null);
373
455
 
374
- // Clear TanStack Query cache (in-memory)
456
+ // Clear TanStack Query cache
375
457
  queryClient.clear();
376
458
 
377
- // Clear persisted query cache
459
+ // Clear persisted query cache and session storage
378
460
  if (storage) {
379
461
  try {
380
462
  await clearQueryCache(storage);
463
+ await storage.removeItem(storageKeys.activeSessionId);
464
+ await storage.removeItem(storageKeys.sessionIds);
465
+ await storage.removeItem('oxy_identity_synced').catch(() => { });
381
466
  } catch (error) {
382
- logger('Failed to clear persisted query cache', error);
467
+ if (__DEV__) {
468
+ logger('Failed to clear session storage', error);
469
+ }
383
470
  }
384
471
  }
472
+ }, [logoutStore, onAuthStateChange, queryClient, storage, storageKeys, logger]);
385
473
 
386
- // Clear session state (sessions, activeSessionId, storage)
387
- await clearSessionState();
474
+ const logout = useCallback(async (targetSessionId?: string): Promise<void> => {
475
+ if (!identityCore || !isCoreInitialized) return;
388
476
 
389
- // Clear identity sync state from storage
390
- if (storage) {
391
- try {
392
- await storage.removeItem('oxy_identity_synced');
393
- } catch (error) {
394
- logger('Failed to clear identity sync state', error);
477
+ try {
478
+ await identityCore.invalidateSession(targetSessionId);
479
+ await loadSessionsFromCore();
480
+ await clearSessionState();
481
+ setTokenReady(false);
482
+ } catch (error) {
483
+ if (__DEV__) {
484
+ logger('Failed to logout', error);
395
485
  }
396
486
  }
487
+ }, [identityCore, isCoreInitialized, loadSessionsFromCore, clearSessionState, setTokenReady, logger]);
397
488
 
398
- // Reset auth store identity sync state
399
- useAuthStore.getState().setIdentitySynced(false);
400
- useAuthStore.getState().setSyncing(false);
489
+ const logoutAll = useCallback(async (): Promise<void> => {
490
+ if (!identityCore || !isCoreInitialized) return;
401
491
 
402
- // Reset account store
403
- useAccountStore.getState().reset();
492
+ try {
493
+ await identityCore.invalidateAllSessions();
494
+ await loadSessionsFromCore();
495
+ await clearSessionState();
496
+ setTokenReady(false);
497
+ } catch (error) {
498
+ if (__DEV__) {
499
+ logger('Failed to logout all', error);
500
+ }
501
+ }
502
+ }, [identityCore, isCoreInitialized, loadSessionsFromCore, clearSessionState, setTokenReady, logger]);
503
+
504
+ const switchSession = useCallback(async (sessionId: string): Promise<User> => {
505
+ if (!identityCore || !isCoreInitialized) {
506
+ throw new Error('Identity core not initialized');
507
+ }
508
+
509
+ try {
510
+ // Validate session
511
+ const validation = await oxyServices.validateSession(sessionId, { useHeaderValidation: true });
512
+ if (!validation?.valid || !validation.user) {
513
+ throw new Error('Session is invalid or expired');
514
+ }
515
+
516
+ const user = validation.user;
517
+
518
+ // Get token for this session
519
+ await oxyServices.getTokenBySession(sessionId);
520
+
521
+ // Update active session in core
522
+ const sessionManager = identityCore.getSessionManager();
523
+ await sessionManager.setActiveSessionId(sessionId);
524
+
525
+ // Update local state
526
+ setActiveSessionId(sessionId);
527
+ loginSuccess(user);
528
+ await applyLanguagePreference(user);
529
+ onAuthStateChange?.(user);
530
+ setTokenReady(true);
531
+ await loadSessionsFromCore();
532
+
533
+ return user;
534
+ } catch (error) {
535
+ if (isInvalidSessionError(error)) {
536
+ // Remove invalid session from list
537
+ setSessions(prev => prev.filter(s => s.sessionId !== sessionId));
538
+ if (sessionId === activeSessionId) {
539
+ await clearSessionState();
540
+ }
541
+ }
542
+ throw error;
543
+ }
544
+ }, [identityCore, isCoreInitialized, oxyServices, loginSuccess, applyLanguagePreference, onAuthStateChange, setTokenReady, loadSessionsFromCore, activeSessionId, clearSessionState]);
545
+
546
+ const refreshSessions = useCallback(async (): Promise<void> => {
547
+ if (!identityCore || !isCoreInitialized || !activeSessionId) return;
548
+
549
+ try {
550
+ // Refresh current session
551
+ await identityCore.refreshSession();
552
+
553
+ // Reload sessions from core
554
+ await loadSessionsFromCore();
555
+ } catch (error) {
556
+ if (isInvalidSessionError(error)) {
557
+ await clearSessionState();
558
+ } else if (__DEV__) {
559
+ logger('Failed to refresh sessions', error);
560
+ }
561
+ }
562
+ }, [identityCore, isCoreInitialized, activeSessionId, loadSessionsFromCore, clearSessionState, logger]);
563
+
564
+ // Device management functions
565
+ const getDeviceSessions = useCallback(async () => {
566
+ if (!activeSessionId) throw new Error('No active session');
567
+ return await oxyServices.getDeviceSessions(activeSessionId);
568
+ }, [activeSessionId, oxyServices]);
569
+
570
+ const logoutAllDeviceSessions = useCallback(async (): Promise<void> => {
571
+ if (!activeSessionId) throw new Error('No active session');
572
+ await oxyServices.logoutAllDeviceSessions(activeSessionId);
573
+ await clearSessionState();
574
+ }, [activeSessionId, oxyServices, clearSessionState]);
575
+
576
+ const updateDeviceName = useCallback(async (deviceName: string): Promise<void> => {
577
+ if (!identityCore || !isCoreInitialized || !activeSessionId) {
578
+ throw new Error('Identity core not initialized or no active session');
579
+ }
580
+
581
+ await oxyServices.updateDeviceName(activeSessionId, deviceName);
582
+
583
+ // Update device name in core
584
+ const deviceManager = identityCore.getDeviceManager();
585
+ await deviceManager.updateDeviceName(deviceName);
586
+ }, [identityCore, isCoreInitialized, activeSessionId, oxyServices]);
587
+
588
+ useTransferCodesPersistence(storageKeyPrefix);
589
+
590
+ const clearAllAccountData = useCallback(async (): Promise<void> => {
591
+ if (__DEV__) logger('Clearing all account data - identity changed or lost');
592
+
593
+ queryClient.clear();
594
+ await clearSessionState();
404
595
 
405
- // CRITICAL: Clear ALL transfer codes and active transfer state
406
- // This prevents transfer state from previous identity from lingering
407
596
  if (storage) {
408
597
  try {
409
- await storage.removeItem(TRANSFER_CODES_STORAGE_KEY);
410
- await storage.removeItem(ACTIVE_TRANSFER_STORAGE_KEY);
411
-
412
- // Also clear Zustand transfer store
413
- useTransferStore.getState().clearAll();
414
-
415
- if (__DEV__) {
416
- logger('Cleared all transfer state');
417
- }
598
+ await clearQueryCache(storage);
599
+ await storage.removeItem('oxy_identity_synced');
418
600
  } catch (error) {
419
- logger('Failed to clear transfer state', error);
601
+ logger('Failed to clear persisted data', error);
420
602
  }
421
603
  }
422
604
 
423
- // Clear HTTP service cache
605
+ useAuthStore.getState().setIdentitySynced(false);
606
+ useAuthStore.getState().setSyncing(false);
607
+ useAccountStore.getState().reset();
608
+ useTransferStore.getState().clearAll();
424
609
  oxyServices.clearCache();
425
-
426
- // Force KeyManager cache invalidation
427
- KeyManager.invalidateCache();
428
- }, [queryClient, storage, clearSessionState, logger, oxyServices, TRANSFER_CODES_STORAGE_KEY, ACTIVE_TRANSFER_STORAGE_KEY]);
429
-
430
- // Extract Zustand store functions early (before they're used in callbacks)
431
- const getAllPendingTransfersStore = useTransferStore((state) => state.getAllPendingTransfers);
432
- const getActiveTransferIdStore = useTransferStore((state) => state.getActiveTransferId);
433
- const storeTransferCodeStore = useTransferStore((state) => state.storeTransferCode);
434
- const getTransferCodeStore = useTransferStore((state) => state.getTransferCode);
435
- const updateTransferStateStore = useTransferStore((state) => state.updateTransferState);
436
- const clearTransferCodeStore = useTransferStore((state) => state.clearTransferCode);
437
-
438
- // Transfer code management functions (must be defined before deleteIdentityAndClearAccount)
439
- const getAllPendingTransfers = useCallback(() => {
440
- return getAllPendingTransfersStore();
441
- }, [getAllPendingTransfersStore]);
442
-
443
- const getActiveTransferId = useCallback(() => {
444
- return getActiveTransferIdStore();
445
- }, [getActiveTransferIdStore]);
610
+ identityCore?.getIdentityManager().invalidateCache();
611
+ }, [queryClient, storage, clearSessionState, logger, oxyServices, identityCore]);
612
+
613
+ // Get transfer store functions (used in deleteIdentityAndClearAccount)
614
+ const getAllPendingTransfers = useTransferStore((state) => state.getAllPendingTransfers);
615
+ const getActiveTransferId = useTransferStore((state) => state.getActiveTransferId);
616
+ const getTransferCode = useTransferStore((state) => state.getTransferCode);
446
617
 
447
618
  // Delete identity and clear all account data
448
619
  // In accounts app, deleting identity means losing the account completely
@@ -458,7 +629,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
458
629
  if (pendingTransfers.length > 0) {
459
630
  const activeTransferId = getActiveTransferId();
460
631
  const hasActiveTransfer = activeTransferId && pendingTransfers.some((t: { transferId: string; data: any }) => t.transferId === activeTransferId);
461
-
632
+
462
633
  if (hasActiveTransfer) {
463
634
  throw new Error(
464
635
  'Cannot delete identity: An active identity transfer is in progress. ' +
@@ -473,335 +644,56 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
473
644
  await clearAllAccountData();
474
645
 
475
646
  // Then delete the identity keys
476
- await KeyManager.deleteIdentity(skipBackup, force, userConfirmed);
477
- }, [clearAllAccountData, getAllPendingTransfers, getActiveTransferId]);
478
-
479
- // Network reconnect sync - TanStack Query automatically retries mutations on reconnect
480
- // We only need to sync identity if it's not synced
481
- useEffect(() => {
482
- if (!storage) return;
483
-
484
- let wasOffline = false;
485
- let checkTimeout: NodeJS.Timeout | null = null;
486
- let lastReconnectionLog = 0;
487
- const RECONNECTION_LOG_DEBOUNCE_MS = 5000; // 5 seconds
488
-
489
- // Circuit breaker and exponential backoff state
490
- const stateRef = {
491
- consecutiveFailures: 0,
492
- currentInterval: 10000, // Start with 10 seconds
493
- baseInterval: 10000, // Base interval in milliseconds
494
- maxInterval: 60000, // Maximum interval (60 seconds)
495
- maxFailures: 5, // Circuit breaker threshold
496
- };
497
-
498
- const scheduleNextCheck = () => {
499
- if (checkTimeout) {
500
- clearTimeout(checkTimeout);
501
- }
502
- checkTimeout = setTimeout(() => {
503
- checkNetworkAndSync();
504
- }, stateRef.currentInterval);
505
- };
506
-
507
- const checkNetworkAndSync = async () => {
508
- try {
509
- // Try a lightweight health check to see if we're online
510
- await oxyServices.healthCheck().catch(() => {
511
- wasOffline = true;
512
- throw new Error('Health check failed');
513
- });
514
-
515
- // Health check succeeded - reset circuit breaker and backoff
516
- if (stateRef.consecutiveFailures > 0) {
517
- stateRef.consecutiveFailures = 0;
518
- stateRef.currentInterval = stateRef.baseInterval;
519
- }
520
-
521
- // If we were offline and now we're online, sync identity if needed
522
- if (wasOffline) {
523
- const now = Date.now();
524
- const timeSinceLastLog = now - lastReconnectionLog;
525
-
526
- if (timeSinceLastLog >= RECONNECTION_LOG_DEBOUNCE_MS) {
527
- logger('Network reconnected, checking identity sync...');
528
- lastReconnectionLog = now;
529
-
530
- // Sync identity first (if not synced)
531
- try {
532
- const hasIdentityValue = await hasIdentity();
533
- if (hasIdentityValue) {
534
- // Check sync status directly - sync if not explicitly 'true'
535
- // undefined = not synced yet, 'false' = explicitly not synced, 'true' = synced
536
- const syncStatus = await storage.getItem('oxy_identity_synced');
537
- if (syncStatus !== 'true') {
538
- await syncIdentity();
539
- }
540
- }
541
- } catch (syncError: any) {
542
- // Skip sync silently if username is required (expected when offline onboarding)
543
- if (syncError?.code === 'USERNAME_REQUIRED' || syncError?.message === 'USERNAME_REQUIRED') {
544
- if (__DEV__) {
545
- loggerUtil.debug('Sync skipped - username required', { component: 'OxyContext', method: 'checkNetworkAndSync' }, syncError as unknown);
546
- }
547
- // Don't log or show error - username will be set later
548
- } else if (!isTimeoutOrNetworkError(syncError)) {
549
- // Only log unexpected errors - timeouts/network issues are expected when offline
550
- logger('Error syncing identity on reconnect', syncError);
551
- } else if (__DEV__) {
552
- loggerUtil.debug('Identity sync timeout (expected when offline)', { component: 'OxyContext', method: 'checkNetworkAndSync' }, syncError as unknown);
553
- }
554
- }
555
-
556
- // Check for pending transfers that may have completed while offline
557
- // This is handled by useCheckPendingTransfers hook which runs automatically
558
- // when authenticated and online
559
- }
560
-
561
- // TanStack Query will automatically retry pending mutations
562
- // Reset flag immediately after processing (whether logged or not)
563
- wasOffline = false;
564
- }
565
- } catch (error) {
566
- // Network check failed - we're offline
567
- wasOffline = true;
568
-
569
- // Increment failure count and apply exponential backoff
570
- stateRef.consecutiveFailures++;
571
-
572
- // Calculate new interval with exponential backoff, capped at maxInterval
573
- const backoffMultiplier = Math.min(
574
- Math.pow(2, stateRef.consecutiveFailures - 1),
575
- stateRef.maxInterval / stateRef.baseInterval
576
- );
577
- stateRef.currentInterval = Math.min(
578
- stateRef.baseInterval * backoffMultiplier,
579
- stateRef.maxInterval
580
- );
581
-
582
- // If we hit the circuit breaker threshold, use max interval
583
- if (stateRef.consecutiveFailures >= stateRef.maxFailures) {
584
- stateRef.currentInterval = stateRef.maxInterval;
585
- }
586
- } finally {
587
- // Always schedule next check (will use updated interval)
588
- scheduleNextCheck();
589
- }
590
- };
591
-
592
- // Check immediately
593
- checkNetworkAndSync();
594
-
595
- return () => {
596
- if (checkTimeout) {
597
- clearTimeout(checkTimeout);
598
- }
599
- };
600
- }, [oxyServices, storage, syncIdentity, logger, hasIdentity]);
601
-
602
- const { getDeviceSessions, logoutAllDeviceSessions, updateDeviceName } = useDeviceManagement({
603
- oxyServices,
604
- activeSessionId,
605
- onError,
606
- clearSessionState,
607
- logger,
608
- });
609
-
610
- const useFollowHook = loadUseFollowHook();
647
+ if (identityCore) {
648
+ await identityCore.deleteIdentity(skipBackup, force, userConfirmed);
649
+ } else {
650
+ throw new Error('Identity core not initialized');
651
+ }
652
+ }, [clearAllAccountData, identityCore, getAllPendingTransfers, getActiveTransferId]);
611
653
 
612
654
  const restoreSessionsFromStorage = useCallback(async (): Promise<void> => {
613
- if (!storage) {
614
- return;
615
- }
655
+ if (!identityCore || !isCoreInitialized) return;
616
656
 
617
657
  setTokenReady(false);
618
-
619
658
  try {
620
- // CRITICAL: Get current identity's public key first
621
- // Only restore sessions that belong to this identity
622
- // Force cache invalidation to ensure we get the actual current identity
623
- KeyManager.invalidateCache();
624
- const currentPublicKey = await KeyManager.getPublicKey().catch(() => null);
625
-
626
- const storedSessionIdsJson = await storage.getItem(storageKeys.sessionIds);
627
- const storedSessionIds: string[] = storedSessionIdsJson ? JSON.parse(storedSessionIdsJson) : [];
628
- const storedActiveSessionId = await storage.getItem(storageKeys.activeSessionId);
629
-
630
- // If no identity exists, clear all sessions and return
631
- if (!currentPublicKey) {
632
- if (storedSessionIds.length > 0 || storedActiveSessionId) {
633
- if (__DEV__) {
634
- logger('No identity found - clearing all stored sessions to prevent cross-identity data leak');
635
- }
636
- await clearSessionState();
637
- }
659
+ await loadSessionsFromCore();
660
+ const activeId = await identityCore.getActiveSessionId();
661
+ if (!activeId) {
638
662
  setTokenReady(true);
639
663
  return;
640
664
  }
641
665
 
642
- if (__DEV__) {
643
- logger('Restoring sessions for identity', { publicKey: currentPublicKey.substring(0, 16) + '...', sessionCount: storedSessionIds.length });
644
- }
645
-
646
- const validSessions: ClientSession[] = [];
647
- const invalidSessionIds: string[] = []; // Track invalid sessions for batch removal
648
-
649
- if (storedSessionIds.length > 0) {
650
- for (const sessionId of storedSessionIds) {
666
+ try {
667
+ await switchSession(activeId);
668
+ } catch (switchError) {
669
+ if (isTimeoutOrNetworkError(switchError)) {
651
670
  try {
652
- const validation = await oxyServices.validateSession(sessionId, { useHeaderValidation: true });
671
+ const validation = await oxyServices.validateSession(activeId, { useHeaderValidation: false });
653
672
  if (validation?.valid && validation.user) {
654
- // CRITICAL: Verify session belongs to current identity
655
- // Compare session's publicKey to current identity's publicKey
656
- if (validation.user.publicKey !== currentPublicKey) {
657
- // Session belongs to different identity - skip it and log warning
658
- if (__DEV__) {
659
- logger('CRITICAL: Skipping session from different identity during restoration', {
660
- sessionPublicKey: validation.user.publicKey?.substring(0, 16) + '...',
661
- currentPublicKey: currentPublicKey.substring(0, 16) + '...',
662
- sessionId: sessionId.substring(0, 16) + '...',
663
- });
664
- }
665
- // Mark for batch removal
666
- invalidSessionIds.push(sessionId);
667
- continue;
668
- }
669
-
670
- const now = new Date();
671
- validSessions.push({
672
- sessionId,
673
- deviceId: '',
674
- expiresAt: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
675
- lastActive: now.toISOString(),
676
- userId: validation.user.id?.toString() ?? '',
677
- isCurrent: sessionId === storedActiveSessionId,
678
- });
679
- }
680
- } catch (validationError) {
681
- // Silently handle expected errors (invalid sessions, timeouts, network issues) during restoration
682
- // Only log unexpected errors
683
- if (!isInvalidSessionError(validationError) && !isTimeoutOrNetworkError(validationError)) {
684
- logger('Session validation failed during init', validationError);
685
- } else if (__DEV__ && isTimeoutOrNetworkError(validationError)) {
686
- // Only log timeouts in dev mode for debugging
687
- loggerUtil.debug('Session validation timeout (expected when offline)', { component: 'OxyContext', method: 'restoreSessionsFromStorage' }, validationError as unknown);
688
- }
689
- }
690
- }
691
-
692
- // Batch remove invalid sessions from storage (performance optimization)
693
- if (invalidSessionIds.length > 0) {
694
- try {
695
- // Use Set for O(n) lookup instead of O(n²) with includes()
696
- const invalidSessionSet = new Set(invalidSessionIds);
697
- const updatedIds = storedSessionIds.filter(id => !invalidSessionSet.has(id));
698
- await storage.setItem(storageKeys.sessionIds, JSON.stringify(updatedIds));
699
- if (__DEV__) {
700
- logger('Removed invalid sessions from storage', { count: invalidSessionIds.length });
701
- }
702
- } catch (cleanupError) {
703
- // Ignore cleanup errors - will be cleaned on next restart
704
- if (__DEV__) {
705
- logger('Failed to remove invalid sessions from storage', cleanupError);
706
- }
707
- }
708
- }
709
-
710
- if (validSessions.length > 0) {
711
- updateSessions(validSessions, { merge: false });
712
- }
713
- }
714
-
715
- if (storedActiveSessionId) {
716
- try {
717
- await switchSession(storedActiveSessionId);
718
- } catch (switchError) {
719
- // Silently handle expected errors (invalid sessions, timeouts, network issues)
720
- if (isInvalidSessionError(switchError)) {
721
- await storage.removeItem(storageKeys.activeSessionId);
722
- updateSessions(
723
- validSessions.filter((session) => session.sessionId !== storedActiveSessionId),
724
- { merge: false },
725
- );
726
- // Don't log expected session errors during restoration
727
- } else if (isTimeoutOrNetworkError(switchError)) {
728
- // Timeout/network error - non-critical, don't block
729
- // However, if we have valid sessions, we should still set activeSessionId
730
- // so that isAuthenticated can be computed correctly
731
- if (validSessions.length > 0) {
732
- const matchingSession = validSessions.find(s => s.sessionId === storedActiveSessionId);
733
- if (matchingSession) {
734
- // Set active session even if validation timed out (offline scenario)
735
- setActiveSessionIdFromHook(storedActiveSessionId);
736
- // Try to get user from session if possible (might fail offline, but that's OK)
737
- try {
738
- const validation = await oxyServices.validateSession(storedActiveSessionId, { useHeaderValidation: false });
739
- if (validation?.valid && validation.user) {
740
- loginSuccess(validation.user);
741
- }
742
- } catch {
743
- // Ignore - we're offline, will sync when online
744
- }
745
- }
746
- }
747
- if (__DEV__) {
748
- loggerUtil.debug('Active session validation timeout (expected when offline)', { component: 'OxyContext', method: 'restoreSessionsFromStorage' }, switchError as unknown);
749
- }
750
- } else {
751
- // Only log unexpected errors
752
- logger('Active session validation error', switchError);
753
- }
754
- }
755
- } else if (validSessions.length > 0) {
756
- // No stored active session, but we have valid sessions - activate the first one
757
- const firstSession = validSessions[0];
758
- try {
759
- await switchSession(firstSession.sessionId);
760
- } catch (switchError) {
761
- // If switch fails, at least set the activeSessionId so UI can show sessions
762
- if (isTimeoutOrNetworkError(switchError)) {
763
- setActiveSessionIdFromHook(firstSession.sessionId);
764
- // Try to get user from session
765
- try {
766
- const validation = await oxyServices.validateSession(firstSession.sessionId, { useHeaderValidation: false });
767
- if (validation?.valid && validation.user) {
768
- loginSuccess(validation.user);
769
- }
770
- } catch {
771
- // Ignore - offline scenario
673
+ loginSuccess(validation.user);
772
674
  }
675
+ } catch {
676
+ // Offline - will sync when online
773
677
  }
678
+ } else if (isInvalidSessionError(switchError)) {
679
+ await identityCore.invalidateSession(activeId);
680
+ await loadSessionsFromCore();
774
681
  }
775
682
  }
776
683
  } catch (error) {
777
- if (__DEV__) {
778
- loggerUtil.error('Auth init error', error instanceof Error ? error : new Error(String(error)), { component: 'OxyContext', method: 'restoreSessionsFromStorage' });
779
- }
684
+ if (__DEV__) logger('Failed to restore sessions from storage', error);
780
685
  await clearSessionState();
781
686
  } finally {
782
687
  setTokenReady(true);
783
688
  }
784
- }, [
785
- clearSessionState,
786
- logger,
787
- oxyServices,
788
- storage,
789
- storageKeys.activeSessionId,
790
- storageKeys.sessionIds,
791
- switchSession,
792
- updateSessions,
793
- setActiveSessionIdFromHook,
794
- loginSuccess,
795
- ]);
689
+ }, [identityCore, isCoreInitialized, loadSessionsFromCore, switchSession, loginSuccess, clearSessionState, oxyServices, logger]);
796
690
 
691
+ // Restore sessions when core is initialized
797
692
  useEffect(() => {
798
- if (!storage || initializedRef.current) {
799
- return;
693
+ if (isCoreInitialized) {
694
+ void restoreSessionsFromStorage();
800
695
  }
801
-
802
- initializedRef.current = true;
803
- void restoreSessionsFromStorage();
804
- }, [restoreSessionsFromStorage, storage]);
696
+ }, [isCoreInitialized, restoreSessionsFromStorage]);
805
697
 
806
698
  const activeSession = activeSessionId
807
699
  ? sessions.find((session) => session.sessionId === activeSessionId)
@@ -810,359 +702,53 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
810
702
 
811
703
  // Compute isAuthenticated from actual session state, not just auth store
812
704
  // This ensures UI shows correct state even if loginSuccess wasn't called during restoration
813
- const isAuthenticatedFromSessions = useMemo(() => {
814
- return !!(activeSessionId && sessions.length > 0 && user);
815
- }, [activeSessionId, sessions.length, user]);
816
-
817
- // Use session-based authentication state if auth store says not authenticated but we have sessions
818
- // This handles the case where sessions were restored but loginSuccess wasn't called
819
705
  const computedIsAuthenticated = useMemo(() => {
820
- return isAuthenticatedFromStore || isAuthenticatedFromSessions;
821
- }, [isAuthenticatedFromStore, isAuthenticatedFromSessions]);
706
+ return isAuthenticatedFromStore || !!(activeSessionId && sessions.length > 0 && user);
707
+ }, [isAuthenticatedFromStore, activeSessionId, sessions.length, user]);
822
708
 
823
- // Get userId from JWT token (MongoDB ObjectId) for socket room matching
824
- // user.id contains the MongoDB ObjectId, user.publicKey contains the cryptographic public key
825
709
  const userId = oxyServices.getCurrentUserId() || user?.id;
826
710
 
827
- // Use Zustand store for transfer state management
828
- // Storage keys are defined above (TRANSFER_CODES_STORAGE_KEY, ACTIVE_TRANSFER_STORAGE_KEY)
829
- const isRestored = useTransferStore((state) => state.isRestored);
830
- const restoreFromStorage = useTransferStore((state) => state.restoreFromStorage);
831
- const markRestored = useTransferStore((state) => state.markRestored);
832
- const cleanupExpired = useTransferStore((state) => state.cleanupExpired);
833
-
834
- // Load transfer codes from storage on startup (only once)
835
- useEffect(() => {
836
- if (!storage || !isStorageReady || isRestored) return;
837
-
838
- const loadTransferCodes = async () => {
839
- try {
840
- // Load transfer codes
841
- const storedCodes = await storage.getItem(TRANSFER_CODES_STORAGE_KEY);
842
- const storedActiveTransferId = await storage.getItem(ACTIVE_TRANSFER_STORAGE_KEY);
843
-
844
- const parsedCodes = storedCodes ? JSON.parse(storedCodes) : {};
845
- const activeTransferId = storedActiveTransferId || null;
846
-
847
- // Restore to Zustand store (store handles validation and expiration)
848
- restoreFromStorage(parsedCodes, activeTransferId);
849
- markRestored();
850
-
851
- if (__DEV__ && Object.keys(parsedCodes).length > 0) {
852
- logger('Restored transfer codes from storage', {
853
- count: Object.keys(parsedCodes).length,
854
- hasActiveTransfer: !!activeTransferId,
855
- });
856
- }
857
- } catch (error) {
858
- if (__DEV__) {
859
- logger('Failed to load transfer codes from storage', error);
860
- }
861
- // Mark as restored even on error to prevent retries
862
- markRestored();
863
- }
864
- };
865
-
866
- loadTransferCodes();
867
- }, [storage, isStorageReady, isRestored, restoreFromStorage, markRestored, logger, storageKeyPrefix]);
868
-
869
- // Persist transfer codes to storage whenever store changes
870
- const { transferCodes, activeTransferId } = useTransferCodesForPersistence();
871
- useEffect(() => {
872
- if (!storage || !isStorageReady || !isRestored) return;
873
-
874
- const persistTransferCodes = async () => {
875
- try {
876
- await storage.setItem(TRANSFER_CODES_STORAGE_KEY, JSON.stringify(transferCodes));
877
-
878
- if (activeTransferId) {
879
- await storage.setItem(ACTIVE_TRANSFER_STORAGE_KEY, activeTransferId);
880
- } else {
881
- await storage.removeItem(ACTIVE_TRANSFER_STORAGE_KEY);
882
- }
883
- } catch (error) {
884
- if (__DEV__) {
885
- logger('Failed to persist transfer codes', error);
886
- }
887
- }
888
- };
889
-
890
- persistTransferCodes();
891
- }, [transferCodes, activeTransferId, storage, isStorageReady, isRestored, logger]);
892
-
893
- // Cleanup expired transfer codes (every minute)
894
- useEffect(() => {
895
- const cleanup = setInterval(() => {
896
- cleanupExpired();
897
- }, 60000); // Check every minute
898
-
899
- return () => clearInterval(cleanup);
900
- }, [cleanupExpired]);
901
-
902
- // Transfer code management functions using Zustand store
903
- const storeTransferCode = useCallback(async (transferId: string, code: string, sourceDeviceId: string | null, publicKey: string) => {
904
- storeTransferCodeStore(transferId, code, sourceDeviceId, publicKey);
905
-
906
- if (__DEV__) {
907
- logger('Stored transfer code', { transferId, sourceDeviceId, publicKey: publicKey.substring(0, 16) + '...' });
908
- }
909
- }, [logger, storeTransferCodeStore]);
910
-
911
- const getTransferCode = useCallback((transferId: string) => {
912
- return getTransferCodeStore(transferId);
913
- }, [getTransferCodeStore]);
914
-
915
- const updateTransferState = useCallback(async (transferId: string, state: 'pending' | 'completed' | 'failed') => {
916
- updateTransferStateStore(transferId, state);
917
-
918
- if (__DEV__) {
919
- logger('Updated transfer state', { transferId, state });
920
- }
921
- }, [logger, updateTransferStateStore]);
922
-
923
- const clearTransferCode = useCallback(async (transferId: string) => {
924
- clearTransferCodeStore(transferId);
925
-
926
- if (__DEV__) {
927
- logger('Cleared transfer code', { transferId });
928
- }
929
- }, [logger, clearTransferCodeStore]);
930
-
931
- const refreshSessionsWithUser = useCallback(
932
- () => refreshSessions(userId),
933
- [refreshSessions, userId],
934
- );
935
-
936
- const handleSessionRemoved = useCallback(
937
- (sessionId: string) => {
938
- trackRemovedSession(sessionId);
939
- },
940
- [trackRemovedSession],
941
- );
942
-
943
- const handleRemoteSignOut = useCallback(() => {
944
- toast.info('You have been signed out remotely.');
945
- logout().catch((remoteError) => logger('Failed to process remote sign out', remoteError));
946
- }, [logger, logout]);
947
-
948
- // Check pending transfers when authenticated and online using TanStack Query
949
- // Check for pending transfers that may have completed while offline
950
- // Results are processed via socket events (onIdentityTransferComplete), so we don't need to process query results here
951
711
  useCheckPendingTransfers(oxyServices, computedIsAuthenticated);
952
712
 
953
- const handleIdentityTransferComplete = useCallback(
954
- async (data: { transferId: string; sourceDeviceId: string; publicKey: string; transferCode?: string; completedAt: string }) => {
955
- try {
956
- logger('Received identity transfer complete notification', {
957
- transferId: data.transferId,
958
- sourceDeviceId: data.sourceDeviceId,
959
- currentDeviceId,
960
- hasActiveSession: activeSessionId !== null,
961
- publicKey: data.publicKey.substring(0, 16) + '...',
962
- });
963
-
964
- const storedTransfer = getTransferCode(data.transferId);
965
-
966
- if (!storedTransfer) {
967
- logger('Transfer code not found for transferId', {
968
- transferId: data.transferId,
969
- });
970
- toast.error('Transfer verification failed: Code not found. Identity will not be deleted.');
971
- return;
972
- }
973
-
974
- // Verify publicKey matches first (most important check)
975
- const publicKeyMatches = data.publicKey === storedTransfer.publicKey;
976
- if (!publicKeyMatches) {
977
- logger('Public key mismatch for transfer', {
978
- transferId: data.transferId,
979
- receivedPublicKey: data.publicKey.substring(0, 16) + '...',
980
- storedPublicKey: storedTransfer.publicKey.substring(0, 16) + '...',
981
- });
982
- toast.error('Transfer verification failed: Public key mismatch. Identity will not be deleted.');
983
- return;
984
- }
985
-
986
- // Verify deviceId matches - very lenient since publicKey is the critical check
987
- // If publicKey matches, we allow deletion even if deviceId doesn't match exactly
988
- // This handles cases where deviceId might not be available or slightly different
989
- const deviceIdMatches =
990
- // Exact match
991
- (data.sourceDeviceId && data.sourceDeviceId === currentDeviceId) ||
992
- // Stored sourceDeviceId matches current deviceId
993
- (storedTransfer.sourceDeviceId && storedTransfer.sourceDeviceId === currentDeviceId);
994
-
995
- // If publicKey matches, we're very lenient with deviceId - only warn but don't block
996
- if (!deviceIdMatches && publicKeyMatches) {
997
- logger('Device ID mismatch for transfer, but publicKey matches - proceeding with deletion', {
998
- transferId: data.transferId,
999
- receivedDeviceId: data.sourceDeviceId,
1000
- storedDeviceId: storedTransfer.sourceDeviceId,
1001
- currentDeviceId,
1002
- hasActiveSession: activeSessionId !== null,
1003
- });
1004
- // Proceed with deletion - publicKey match is the critical verification
1005
- } else if (!deviceIdMatches && !publicKeyMatches) {
1006
- // Both don't match - this is suspicious, block deletion
1007
- logger('Device ID and publicKey mismatch for transfer', {
1008
- transferId: data.transferId,
1009
- receivedDeviceId: data.sourceDeviceId,
1010
- currentDeviceId,
1011
- });
1012
- toast.error('Transfer verification failed: Device and key mismatch. Identity will not be deleted.');
1013
- return;
1014
- }
1015
-
1016
- // Verify transfer code matches (if provided)
1017
- // Transfer code is optional - if not provided, we still proceed if publicKey matches
1018
- if (data.transferCode) {
1019
- const codeMatches = data.transferCode.toUpperCase() === storedTransfer.code.toUpperCase();
1020
- if (!codeMatches) {
1021
- logger('Transfer code mismatch, but publicKey matches - proceeding with deletion', {
1022
- transferId: data.transferId,
1023
- receivedCode: data.transferCode,
1024
- storedCode: storedTransfer.code.substring(0, 2) + '****',
1025
- });
1026
- // Don't block - publicKey match is sufficient, code mismatch might be due to user error
1027
- // Log warning but proceed
1028
- }
1029
- }
1030
-
1031
- // Check if transfer is too old (safety timeout - 10 minutes)
1032
- const transferAge = Date.now() - storedTransfer.timestamp;
1033
- const tenMinutes = 10 * 60 * 1000;
1034
- if (transferAge > tenMinutes) {
1035
- logger('Transfer confirmation received too late', {
1036
- transferId: data.transferId,
1037
- age: transferAge,
1038
- ageMinutes: Math.round(transferAge / 60000),
1039
- });
1040
- toast.error('Transfer verification failed: Confirmation received too late. Identity will not be deleted.');
1041
- clearTransferCode(data.transferId);
1042
- return;
1043
- }
1044
-
1045
- // NOTE: Target device verification already happened server-side when notifyTransferComplete was called
1046
- // The server verified that the target device is authenticated and has the matching public key
1047
- // Additional client-side verification is not necessary and would require source device authentication
1048
- // which may not be available. The existing checks (public key match, transfer code, device ID) are sufficient.
1049
-
1050
- logger('All transfer verifications passed, deleting identity from source device', {
1051
- transferId: data.transferId,
1052
- sourceDeviceId: data.sourceDeviceId,
1053
- publicKey: data.publicKey.substring(0, 16) + '...',
1054
- });
1055
-
1056
- try {
1057
- // Verify identity still exists before deletion (safety check)
1058
- const identityStillExists = await KeyManager.hasIdentity();
1059
- if (!identityStillExists) {
1060
- logger('Identity already deleted - skipping deletion', {
1061
- transferId: data.transferId,
1062
- });
1063
- await updateTransferState(data.transferId, 'completed');
1064
- await clearTransferCode(data.transferId);
1065
- return;
1066
- }
1067
-
1068
- await deleteIdentityAndClearAccount(false, false, true);
1069
-
1070
- // Verify identity was actually deleted
1071
- const identityDeleted = !(await KeyManager.hasIdentity());
1072
- if (!identityDeleted) {
1073
- logger('Identity deletion failed - identity still exists', {
1074
- transferId: data.transferId,
1075
- });
1076
- await updateTransferState(data.transferId, 'failed');
1077
- throw new Error('Identity deletion failed - identity still exists');
1078
- }
1079
-
1080
- await updateTransferState(data.transferId, 'completed');
1081
- await clearTransferCode(data.transferId);
1082
-
1083
- logger('Identity successfully deleted and transfer code cleared', {
1084
- transferId: data.transferId,
1085
- });
1086
-
1087
- toast.success('Identity successfully transferred and removed from this device');
1088
- } catch (deleteError: any) {
1089
- logger('Error during identity deletion', deleteError);
1090
- await updateTransferState(data.transferId, 'failed');
1091
- throw deleteError;
1092
- }
1093
- } catch (error: any) {
1094
- logger('Failed to delete identity after transfer', error);
1095
- toast.error(error?.message || 'Failed to remove identity from this device. Please try again manually from Security Settings.');
1096
- }
1097
- },
1098
- [deleteIdentityAndClearAccount, logger, getTransferCode, clearTransferCode, updateTransferState, currentDeviceId, activeSessionId, oxyServices],
1099
- );
713
+ const { handleIdentityTransferComplete } = useIdentityTransfer({
714
+ identityCore,
715
+ currentDeviceId,
716
+ activeSessionId,
717
+ getPublicKey,
718
+ deleteIdentityAndClearAccount,
719
+ logger,
720
+ });
1100
721
 
1101
722
  useSessionSocket({
1102
723
  userId,
1103
724
  activeSessionId,
1104
725
  currentDeviceId,
1105
- refreshSessions: refreshSessionsWithUser,
726
+ refreshSessions,
1106
727
  logout,
1107
728
  clearSessionState,
1108
729
  baseURL: oxyServices.getBaseURL(),
1109
730
  getAccessToken: () => oxyServices.getAccessToken(),
1110
731
  getTransferCode: getTransferCode,
1111
- onRemoteSignOut: handleRemoteSignOut,
1112
- onSessionRemoved: handleSessionRemoved,
1113
- onIdentityTransferComplete: handleIdentityTransferComplete,
1114
- });
1115
-
1116
- const switchSessionForContext = useCallback(
1117
- async (sessionId: string): Promise<void> => {
1118
- await switchSession(sessionId);
732
+ onRemoteSignOut: () => {
733
+ toast.info('You have been signed out remotely.');
734
+ logout().catch((err) => logger('Failed to process remote sign out', err));
1119
735
  },
1120
- [switchSession],
1121
- );
1122
-
1123
- // Create showBottomSheet function that uses the global function
1124
- const showBottomSheetForContext = useCallback(
1125
- (screenOrConfig: RouteName | { screen: RouteName; props?: Record<string, unknown> }) => {
1126
- globalShowBottomSheet(screenOrConfig);
736
+ onSessionRemoved: (sessionId: string) => {
737
+ setSessions(prev => prev.filter(s => s.sessionId !== sessionId));
738
+ if (sessionId === activeSessionId) {
739
+ setActiveSessionId(null);
740
+ }
1127
741
  },
1128
- [],
1129
- );
742
+ onIdentityTransferComplete: handleIdentityTransferComplete,
743
+ });
1130
744
 
1131
- // Create openAvatarPicker function
1132
- const openAvatarPicker = useCallback(() => {
1133
- showBottomSheetForContext({
1134
- screen: 'FileManagement' as RouteName,
1135
- props: {
1136
- selectMode: true,
1137
- multiSelect: false,
1138
- disabledMimeTypes: ['video/', 'audio/', 'application/pdf'],
1139
- afterSelect: 'none', // Don't navigate away - stay on current screen
1140
- onSelect: async (file: any) => {
1141
- if (!file.contentType.startsWith('image/')) {
1142
- toast.error(translate(currentLanguage, 'editProfile.toasts.selectImage') || 'Please select an image file');
1143
- return;
1144
- }
1145
- try {
1146
- // Update file visibility to public for avatar
1147
- await updateAvatarVisibility(file.id, oxyServices, 'OxyContext');
1148
-
1149
- // Update user profile (handles query invalidation and accountStore update)
1150
- await updateProfileWithAvatar(
1151
- { avatar: file.id },
1152
- oxyServices,
1153
- activeSessionId,
1154
- queryClient,
1155
- syncIdentity
1156
- );
1157
-
1158
- toast.success(translate(currentLanguage, 'editProfile.toasts.avatarUpdated') || 'Avatar updated');
1159
- } catch (e: any) {
1160
- toast.error(e.message || translate(currentLanguage, 'editProfile.toasts.updateAvatarFailed') || 'Failed to update avatar');
1161
- }
1162
- },
1163
- },
1164
- });
1165
- }, [oxyServices, currentLanguage, showBottomSheetForContext, activeSessionId, queryClient, syncIdentity]);
745
+ const { openAvatarPicker } = useAvatarPicker({
746
+ oxyServices,
747
+ currentLanguage,
748
+ activeSessionId,
749
+ queryClient,
750
+ syncIdentity,
751
+ });
1166
752
 
1167
753
  const contextValue: OxyContextState = useMemo(() => ({
1168
754
  user,
@@ -1186,21 +772,15 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1186
772
  isIdentitySynced,
1187
773
  syncIdentity,
1188
774
  deleteIdentityAndClearAccount,
1189
- storeTransferCode,
1190
- getTransferCode,
1191
- clearTransferCode,
1192
- getAllPendingTransfers,
1193
- getActiveTransferId,
1194
- updateTransferState,
1195
775
  identitySyncState: {
1196
776
  isSynced: isIdentitySyncedStore ?? true,
1197
777
  isSyncing: isSyncing ?? false,
1198
778
  },
1199
779
  logout,
1200
780
  logoutAll,
1201
- switchSession: switchSessionForContext,
781
+ switchSession,
1202
782
  removeSession: logout,
1203
- refreshSessions: refreshSessionsWithUser,
783
+ refreshSessions,
1204
784
  setLanguage,
1205
785
  getDeviceSessions,
1206
786
  logoutAllDeviceSessions,
@@ -1208,8 +788,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1208
788
  clearSessionState,
1209
789
  clearAllAccountData,
1210
790
  oxyServices,
1211
- useFollow: useFollowHook,
1212
- showBottomSheet: showBottomSheetForContext,
791
+ showBottomSheet: globalShowBottomSheet,
1213
792
  openAvatarPicker,
1214
793
  }), [
1215
794
  activeSessionId,
@@ -1222,12 +801,6 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1222
801
  isIdentitySynced,
1223
802
  syncIdentity,
1224
803
  deleteIdentityAndClearAccount,
1225
- storeTransferCode,
1226
- getTransferCode,
1227
- clearTransferCode,
1228
- getAllPendingTransfers,
1229
- getActiveTransferId,
1230
- updateTransferState,
1231
804
  isIdentitySyncedStore,
1232
805
  isSyncing,
1233
806
  currentLanguage,
@@ -1242,17 +815,15 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
1242
815
  logoutAll,
1243
816
  logoutAllDeviceSessions,
1244
817
  oxyServices,
1245
- refreshSessionsWithUser,
818
+ refreshSessions,
1246
819
  sessions,
1247
820
  setLanguage,
1248
- switchSessionForContext,
821
+ switchSession,
1249
822
  tokenReady,
1250
823
  isStorageReady,
1251
824
  updateDeviceName,
1252
825
  clearAllAccountData,
1253
- useFollowHook,
1254
826
  user,
1255
- showBottomSheetForContext,
1256
827
  openAvatarPicker,
1257
828
  ]);
1258
829