@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.
- package/lib/commonjs/adapters/expo/crypto.js +56 -0
- package/lib/commonjs/adapters/expo/crypto.js.map +1 -0
- package/lib/commonjs/adapters/expo/fetch.js +30 -0
- package/lib/commonjs/adapters/expo/fetch.js.map +1 -0
- package/lib/commonjs/adapters/expo/index.js +48 -0
- package/lib/commonjs/adapters/expo/index.js.map +1 -0
- package/lib/commonjs/adapters/expo/storage.js +201 -0
- package/lib/commonjs/adapters/expo/storage.js.map +1 -0
- package/lib/commonjs/adapters/index.js +41 -0
- package/lib/commonjs/adapters/index.js.map +1 -0
- package/lib/commonjs/adapters/node/crypto.js +40 -0
- package/lib/commonjs/adapters/node/crypto.js.map +1 -0
- package/lib/commonjs/adapters/node/fetch.js +62 -0
- package/lib/commonjs/adapters/node/fetch.js.map +1 -0
- package/lib/commonjs/adapters/node/index.js +34 -0
- package/lib/commonjs/adapters/node/index.js.map +1 -0
- package/lib/commonjs/adapters/node/storage.js +163 -0
- package/lib/commonjs/adapters/node/storage.js.map +1 -0
- package/lib/commonjs/core/identity-session/DeviceManager.js +237 -0
- package/lib/commonjs/core/identity-session/DeviceManager.js.map +1 -0
- package/lib/commonjs/core/identity-session/INTEGRATION_GUIDE.md +287 -0
- package/lib/commonjs/core/identity-session/IdentityManager.js +400 -0
- package/lib/commonjs/core/identity-session/IdentityManager.js.map +1 -0
- package/lib/commonjs/core/identity-session/IdentitySessionCore.js +394 -0
- package/lib/commonjs/core/identity-session/IdentitySessionCore.js.map +1 -0
- package/lib/commonjs/core/identity-session/RefreshManager.js +137 -0
- package/lib/commonjs/core/identity-session/RefreshManager.js.map +1 -0
- package/lib/commonjs/core/identity-session/SessionManager.js +427 -0
- package/lib/commonjs/core/identity-session/SessionManager.js.map +1 -0
- package/lib/commonjs/core/identity-session/createIdentitySessionCore.js +24 -0
- package/lib/commonjs/core/identity-session/createIdentitySessionCore.js.map +1 -0
- package/lib/commonjs/core/identity-session/errors.js +176 -0
- package/lib/commonjs/core/identity-session/errors.js.map +1 -0
- package/lib/commonjs/core/identity-session/index.js +80 -0
- package/lib/commonjs/core/identity-session/index.js.map +1 -0
- package/lib/commonjs/core/identity-session/types.js +2 -0
- package/lib/commonjs/core/identity-session/types.js.map +1 -0
- package/lib/commonjs/core/index.js +2 -21
- package/lib/commonjs/core/index.js.map +1 -1
- package/lib/commonjs/index.js +58 -8
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/models/interfaces.js +7 -0
- package/lib/commonjs/models/interfaces.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +434 -820
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAvatarPicker.js +52 -0
- package/lib/commonjs/ui/hooks/useAvatarPicker.js.map +1 -0
- package/lib/commonjs/ui/hooks/useIdentityTransfer.js +125 -0
- package/lib/commonjs/ui/hooks/useIdentityTransfer.js.map +1 -0
- package/lib/commonjs/ui/hooks/useTransferCodesPersistence.js +81 -0
- package/lib/commonjs/ui/hooks/useTransferCodesPersistence.js.map +1 -0
- package/lib/commonjs/ui/screens/AccountCenterScreen.js +7 -2
- package/lib/commonjs/ui/screens/AccountCenterScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js +12 -5
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +2 -2
- package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/ProfileScreen.js +6 -6
- package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
- package/lib/commonjs/ui/utils/sessionHelpers.js +7 -1
- package/lib/commonjs/ui/utils/sessionHelpers.js.map +1 -1
- package/lib/commonjs/utils/index.js +0 -7
- package/lib/commonjs/utils/index.js.map +1 -1
- package/lib/commonjs/utils/sessionUtils.js +8 -1
- package/lib/commonjs/utils/sessionUtils.js.map +1 -1
- package/lib/module/adapters/expo/crypto.js +51 -0
- package/lib/module/adapters/expo/crypto.js.map +1 -0
- package/lib/module/adapters/expo/fetch.js +26 -0
- package/lib/module/adapters/expo/fetch.js.map +1 -0
- package/lib/module/adapters/expo/index.js +45 -0
- package/lib/module/adapters/expo/index.js.map +1 -0
- package/lib/module/adapters/expo/storage.js +198 -0
- package/lib/module/adapters/expo/storage.js.map +1 -0
- package/lib/module/adapters/index.js +38 -0
- package/lib/module/adapters/index.js.map +1 -0
- package/lib/module/adapters/node/crypto.js +36 -0
- package/lib/module/adapters/node/crypto.js.map +1 -0
- package/lib/module/adapters/node/fetch.js +57 -0
- package/lib/module/adapters/node/fetch.js.map +1 -0
- package/lib/module/adapters/node/index.js +31 -0
- package/lib/module/adapters/node/index.js.map +1 -0
- package/lib/module/adapters/node/storage.js +159 -0
- package/lib/module/adapters/node/storage.js.map +1 -0
- package/lib/module/core/identity-session/DeviceManager.js +232 -0
- package/lib/module/core/identity-session/DeviceManager.js.map +1 -0
- package/lib/module/core/identity-session/INTEGRATION_GUIDE.md +287 -0
- package/lib/module/core/identity-session/IdentityManager.js +395 -0
- package/lib/module/core/identity-session/IdentityManager.js.map +1 -0
- package/lib/module/core/identity-session/IdentitySessionCore.js +390 -0
- package/lib/module/core/identity-session/IdentitySessionCore.js.map +1 -0
- package/lib/module/core/identity-session/RefreshManager.js +132 -0
- package/lib/module/core/identity-session/RefreshManager.js.map +1 -0
- package/lib/module/core/identity-session/SessionManager.js +422 -0
- package/lib/module/core/identity-session/SessionManager.js.map +1 -0
- package/lib/module/core/identity-session/createIdentitySessionCore.js +21 -0
- package/lib/module/core/identity-session/createIdentitySessionCore.js.map +1 -0
- package/lib/module/core/identity-session/errors.js +170 -0
- package/lib/module/core/identity-session/errors.js.map +1 -0
- package/lib/module/core/identity-session/index.js +17 -0
- package/lib/module/core/identity-session/index.js.map +1 -0
- package/lib/module/core/identity-session/types.js +2 -0
- package/lib/module/core/identity-session/types.js.map +1 -0
- package/lib/module/core/index.js +2 -3
- package/lib/module/core/index.js.map +1 -1
- package/lib/module/index.js +12 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/models/interfaces.js +7 -0
- package/lib/module/models/interfaces.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +436 -822
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/useAvatarPicker.js +48 -0
- package/lib/module/ui/hooks/useAvatarPicker.js.map +1 -0
- package/lib/module/ui/hooks/useIdentityTransfer.js +121 -0
- package/lib/module/ui/hooks/useIdentityTransfer.js.map +1 -0
- package/lib/module/ui/hooks/useTransferCodesPersistence.js +77 -0
- package/lib/module/ui/hooks/useTransferCodesPersistence.js.map +1 -0
- package/lib/module/ui/screens/AccountCenterScreen.js +7 -2
- package/lib/module/ui/screens/AccountCenterScreen.js.map +1 -1
- package/lib/module/ui/screens/AccountSettingsScreen.js +12 -5
- package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/module/ui/screens/AccountSwitcherScreen.js +2 -2
- package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -1
- package/lib/module/ui/screens/ProfileScreen.js +6 -6
- package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
- package/lib/module/ui/utils/sessionHelpers.js +7 -1
- package/lib/module/ui/utils/sessionHelpers.js.map +1 -1
- package/lib/module/utils/index.js +2 -1
- package/lib/module/utils/index.js.map +1 -1
- package/lib/module/utils/sessionUtils.js +8 -1
- package/lib/module/utils/sessionUtils.js.map +1 -1
- package/lib/typescript/adapters/expo/crypto.d.ts +17 -0
- package/lib/typescript/adapters/expo/crypto.d.ts.map +1 -0
- package/lib/typescript/adapters/expo/fetch.d.ts +16 -0
- package/lib/typescript/adapters/expo/fetch.d.ts.map +1 -0
- package/lib/typescript/adapters/expo/index.d.ts +23 -0
- package/lib/typescript/adapters/expo/index.d.ts.map +1 -0
- package/lib/typescript/adapters/expo/storage.d.ts +23 -0
- package/lib/typescript/adapters/expo/storage.d.ts.map +1 -0
- package/lib/typescript/adapters/index.d.ts +13 -0
- package/lib/typescript/adapters/index.d.ts.map +1 -0
- package/lib/typescript/adapters/node/crypto.d.ts +17 -0
- package/lib/typescript/adapters/node/crypto.d.ts.map +1 -0
- package/lib/typescript/adapters/node/fetch.d.ts +16 -0
- package/lib/typescript/adapters/node/fetch.d.ts.map +1 -0
- package/lib/typescript/adapters/node/index.d.ts +23 -0
- package/lib/typescript/adapters/node/index.d.ts.map +1 -0
- package/lib/typescript/adapters/node/storage.d.ts +23 -0
- package/lib/typescript/adapters/node/storage.d.ts.map +1 -0
- package/lib/typescript/core/identity-session/DeviceManager.d.ts +64 -0
- package/lib/typescript/core/identity-session/DeviceManager.d.ts.map +1 -0
- package/lib/typescript/core/identity-session/IdentityManager.d.ts +88 -0
- package/lib/typescript/core/identity-session/IdentityManager.d.ts.map +1 -0
- package/lib/typescript/core/identity-session/IdentitySessionCore.d.ts +141 -0
- package/lib/typescript/core/identity-session/IdentitySessionCore.d.ts.map +1 -0
- package/lib/typescript/core/identity-session/RefreshManager.d.ts +36 -0
- package/lib/typescript/core/identity-session/RefreshManager.d.ts.map +1 -0
- package/lib/typescript/core/identity-session/SessionManager.d.ts +104 -0
- package/lib/typescript/core/identity-session/SessionManager.d.ts.map +1 -0
- package/lib/typescript/core/identity-session/createIdentitySessionCore.d.ts +11 -0
- package/lib/typescript/core/identity-session/createIdentitySessionCore.d.ts.map +1 -0
- package/lib/typescript/core/identity-session/errors.d.ts +63 -0
- package/lib/typescript/core/identity-session/errors.d.ts.map +1 -0
- package/lib/typescript/core/identity-session/index.d.ts +14 -0
- package/lib/typescript/core/identity-session/index.d.ts.map +1 -0
- package/lib/typescript/core/identity-session/types.d.ts +196 -0
- package/lib/typescript/core/identity-session/types.d.ts.map +1 -0
- package/lib/typescript/core/index.d.ts +1 -3
- package/lib/typescript/core/index.d.ts.map +1 -1
- package/lib/typescript/core/mixins/index.d.ts +2 -2
- package/lib/typescript/index.d.ts +3 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/models/interfaces.d.ts +5 -36
- package/lib/typescript/models/interfaces.d.ts.map +1 -1
- package/lib/typescript/models/session.d.ts +3 -16
- package/lib/typescript/models/session.d.ts.map +1 -1
- package/lib/typescript/ui/context/OxyContext.d.ts +2 -25
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts +7 -8
- package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/mutations/useServicesMutations.d.ts +1 -1
- package/lib/typescript/ui/hooks/mutations/useServicesMutations.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts +5 -5
- package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/useAvatarPicker.d.ts +18 -0
- package/lib/typescript/ui/hooks/useAvatarPicker.d.ts.map +1 -0
- package/lib/typescript/ui/hooks/useIdentityTransfer.d.ts +24 -0
- package/lib/typescript/ui/hooks/useIdentityTransfer.d.ts.map +1 -0
- package/lib/typescript/ui/hooks/useTransferCodesPersistence.d.ts +6 -0
- package/lib/typescript/ui/hooks/useTransferCodesPersistence.d.ts.map +1 -0
- package/lib/typescript/ui/screens/AccountCenterScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
- package/lib/typescript/ui/utils/sessionHelpers.d.ts +1 -0
- package/lib/typescript/ui/utils/sessionHelpers.d.ts.map +1 -1
- package/lib/typescript/utils/index.d.ts +0 -2
- package/lib/typescript/utils/index.d.ts.map +1 -1
- package/lib/typescript/utils/sessionUtils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/adapters/expo/crypto.ts +55 -0
- package/src/adapters/expo/fetch.ts +28 -0
- package/src/adapters/expo/index.ts +51 -0
- package/src/adapters/expo/storage.ts +228 -0
- package/src/adapters/index.ts +40 -0
- package/src/adapters/node/crypto.ts +39 -0
- package/src/adapters/node/fetch.ts +59 -0
- package/src/adapters/node/index.ts +37 -0
- package/src/adapters/node/storage.ts +170 -0
- package/src/core/identity-session/DeviceManager.ts +273 -0
- package/src/core/identity-session/INTEGRATION_GUIDE.md +287 -0
- package/src/core/identity-session/IdentityManager.ts +474 -0
- package/src/core/identity-session/IdentitySessionCore.ts +464 -0
- package/src/core/identity-session/RefreshManager.ts +189 -0
- package/src/core/identity-session/SessionManager.ts +500 -0
- package/src/core/identity-session/createIdentitySessionCore.ts +19 -0
- package/src/core/identity-session/errors.ts +197 -0
- package/src/core/identity-session/index.ts +15 -0
- package/src/core/identity-session/types.ts +188 -0
- package/src/core/index.ts +3 -4
- package/src/index.ts +28 -3
- package/src/models/interfaces.ts +12 -39
- package/src/models/session.ts +6 -16
- package/src/ui/context/OxyContext.tsx +442 -871
- package/src/ui/hooks/auth/index.ts +1 -0
- package/src/ui/hooks/useAvatarPicker.ts +62 -0
- package/src/ui/hooks/useIdentityTransfer.ts +135 -0
- package/src/ui/hooks/useTransferCodesPersistence.ts +80 -0
- package/src/ui/screens/AccountCenterScreen.tsx +7 -2
- package/src/ui/screens/AccountSettingsScreen.tsx +15 -8
- package/src/ui/screens/AccountSwitcherScreen.tsx +2 -2
- package/src/ui/screens/ProfileScreen.tsx +10 -10
- package/src/ui/utils/sessionHelpers.ts +7 -0
- package/src/utils/index.ts +1 -2
- package/src/utils/sessionUtils.ts +8 -0
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +0 -732
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +0 -1
- package/lib/commonjs/ui/context/hooks/useDeviceManagement.js +0 -73
- package/lib/commonjs/ui/context/hooks/useDeviceManagement.js.map +0 -1
- package/lib/commonjs/ui/hooks/useDeviceManagement.js +0 -73
- package/lib/commonjs/ui/hooks/useDeviceManagement.js.map +0 -1
- package/lib/commonjs/ui/hooks/useSessionManagement.js +0 -281
- package/lib/commonjs/ui/hooks/useSessionManagement.js.map +0 -1
- package/lib/commonjs/utils/deviceManager.js +0 -177
- package/lib/commonjs/utils/deviceManager.js.map +0 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +0 -726
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +0 -1
- package/lib/module/ui/context/hooks/useDeviceManagement.js +0 -68
- package/lib/module/ui/context/hooks/useDeviceManagement.js.map +0 -1
- package/lib/module/ui/hooks/useDeviceManagement.js +0 -68
- package/lib/module/ui/hooks/useDeviceManagement.js.map +0 -1
- package/lib/module/ui/hooks/useSessionManagement.js +0 -276
- package/lib/module/ui/hooks/useSessionManagement.js.map +0 -1
- package/lib/module/utils/deviceManager.js +0 -171
- package/lib/module/utils/deviceManager.js.map +0 -1
- package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts +0 -59
- package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts.map +0 -1
- package/lib/typescript/ui/context/hooks/useDeviceManagement.d.ts +0 -27
- package/lib/typescript/ui/context/hooks/useDeviceManagement.d.ts.map +0 -1
- package/lib/typescript/ui/hooks/useDeviceManagement.d.ts +0 -27
- package/lib/typescript/ui/hooks/useDeviceManagement.d.ts.map +0 -1
- package/lib/typescript/ui/hooks/useSessionManagement.d.ts +0 -41
- package/lib/typescript/ui/hooks/useSessionManagement.d.ts.map +0 -1
- package/lib/typescript/utils/deviceManager.d.ts +0 -66
- package/lib/typescript/utils/deviceManager.d.ts.map +0 -1
- package/src/ui/context/hooks/useAuthOperations.ts +0 -801
- package/src/ui/context/hooks/useDeviceManagement.ts +0 -108
- package/src/ui/hooks/useDeviceManagement.ts +0 -108
- package/src/ui/hooks/useSessionManagement.ts +0 -401
- 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 {
|
|
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 {
|
|
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<
|
|
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
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const checkAndRestoreIdentity = async () => {
|
|
179
|
+
let mounted = true;
|
|
180
|
+
const initCore = async () => {
|
|
231
181
|
try {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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('
|
|
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
|
-
|
|
267
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
343
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
373
|
+
[identityCore, isCoreInitialized, oxyServices, storage, loginSuccess, setIdentitySynced, setSyncing, logger]
|
|
361
374
|
);
|
|
362
375
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
387
|
-
|
|
474
|
+
const logout = useCallback(async (targetSessionId?: string): Promise<void> => {
|
|
475
|
+
if (!identityCore || !isCoreInitialized) return;
|
|
388
476
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
useAuthStore.getState().setSyncing(false);
|
|
489
|
+
const logoutAll = useCallback(async (): Promise<void> => {
|
|
490
|
+
if (!identityCore || !isCoreInitialized) return;
|
|
401
491
|
|
|
402
|
-
|
|
403
|
-
|
|
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
|
|
410
|
-
await storage.removeItem(
|
|
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
|
|
601
|
+
logger('Failed to clear persisted data', error);
|
|
420
602
|
}
|
|
421
603
|
}
|
|
422
604
|
|
|
423
|
-
|
|
605
|
+
useAuthStore.getState().setIdentitySynced(false);
|
|
606
|
+
useAuthStore.getState().setSyncing(false);
|
|
607
|
+
useAccountStore.getState().reset();
|
|
608
|
+
useTransferStore.getState().clearAll();
|
|
424
609
|
oxyServices.clearCache();
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
const
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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 (!
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
655
|
+
if (!identityCore || !isCoreInitialized) return;
|
|
616
656
|
|
|
617
657
|
setTokenReady(false);
|
|
618
|
-
|
|
619
658
|
try {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
643
|
-
|
|
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(
|
|
671
|
+
const validation = await oxyServices.validateSession(activeId, { useHeaderValidation: false });
|
|
653
672
|
if (validation?.valid && validation.user) {
|
|
654
|
-
|
|
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 (
|
|
799
|
-
|
|
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 ||
|
|
821
|
-
}, [isAuthenticatedFromStore,
|
|
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 =
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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
|
|
726
|
+
refreshSessions,
|
|
1106
727
|
logout,
|
|
1107
728
|
clearSessionState,
|
|
1108
729
|
baseURL: oxyServices.getBaseURL(),
|
|
1109
730
|
getAccessToken: () => oxyServices.getAccessToken(),
|
|
1110
731
|
getTransferCode: getTransferCode,
|
|
1111
|
-
onRemoteSignOut:
|
|
1112
|
-
|
|
1113
|
-
|
|
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
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
|
781
|
+
switchSession,
|
|
1202
782
|
removeSession: logout,
|
|
1203
|
-
refreshSessions
|
|
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
|
-
|
|
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
|
-
|
|
818
|
+
refreshSessions,
|
|
1246
819
|
sessions,
|
|
1247
820
|
setLanguage,
|
|
1248
|
-
|
|
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
|
|