@oxyhq/services 5.16.0 → 5.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/core/mixins/OxyServices.assets.js +15 -0
- package/lib/commonjs/core/mixins/OxyServices.assets.js.map +1 -1
- package/lib/commonjs/core/mixins/OxyServices.user.js +14 -13
- package/lib/commonjs/core/mixins/OxyServices.user.js.map +1 -1
- package/lib/commonjs/crypto/keyManager.js +164 -3
- package/lib/commonjs/crypto/keyManager.js.map +1 -1
- package/lib/commonjs/crypto/signatureService.js +26 -0
- package/lib/commonjs/crypto/signatureService.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/GroupedSection.js +1 -1
- package/lib/commonjs/ui/components/GroupedSection.js.map +1 -1
- package/lib/commonjs/ui/components/OxyProvider.js +71 -24
- package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
- package/lib/commonjs/ui/components/profile/EditDisplayNameModal.js +1 -4
- package/lib/commonjs/ui/components/profile/EditDisplayNameModal.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +177 -4
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +148 -49
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useSessionManagement.js +22 -2
- package/lib/commonjs/ui/context/hooks/useSessionManagement.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/index.js +28 -0
- package/lib/commonjs/ui/hooks/mutations/index.js.map +1 -0
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +314 -0
- package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -0
- package/lib/commonjs/ui/hooks/mutations/useServicesMutations.js +193 -0
- package/lib/commonjs/ui/hooks/mutations/useServicesMutations.js.map +1 -0
- package/lib/commonjs/ui/hooks/queries/index.js +39 -0
- package/lib/commonjs/ui/hooks/queries/index.js.map +1 -0
- package/lib/commonjs/ui/hooks/queries/queryKeys.js +85 -0
- package/lib/commonjs/ui/hooks/queries/queryKeys.js.map +1 -0
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +145 -0
- package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -0
- package/lib/commonjs/ui/hooks/queries/useServicesQueries.js +138 -0
- package/lib/commonjs/ui/hooks/queries/useServicesQueries.js.map +1 -0
- package/lib/commonjs/ui/hooks/queryClient.js +117 -0
- package/lib/commonjs/ui/hooks/queryClient.js.map +1 -0
- package/lib/commonjs/ui/hooks/useIdentityMutations.js +111 -0
- package/lib/commonjs/ui/hooks/useIdentityMutations.js.map +1 -0
- package/lib/commonjs/ui/hooks/useProfileEditing.js +42 -58
- package/lib/commonjs/ui/hooks/useProfileEditing.js.map +1 -1
- package/lib/commonjs/ui/hooks/useQueryClient.js +20 -0
- package/lib/commonjs/ui/hooks/useQueryClient.js.map +1 -0
- package/lib/commonjs/ui/hooks/useSessionManagement.js +22 -2
- package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountOverviewScreen.js +43 -42
- package/lib/commonjs/ui/screens/AccountOverviewScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js +63 -58
- package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +6 -6
- package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -1
- package/lib/commonjs/ui/stores/accountStore.js +57 -42
- package/lib/commonjs/ui/stores/accountStore.js.map +1 -1
- package/lib/commonjs/ui/stores/authStore.js +4 -25
- package/lib/commonjs/ui/stores/authStore.js.map +1 -1
- package/lib/module/core/mixins/OxyServices.assets.js +15 -0
- package/lib/module/core/mixins/OxyServices.assets.js.map +1 -1
- package/lib/module/core/mixins/OxyServices.user.js +14 -13
- package/lib/module/core/mixins/OxyServices.user.js.map +1 -1
- package/lib/module/crypto/keyManager.js +164 -3
- package/lib/module/crypto/keyManager.js.map +1 -1
- package/lib/module/crypto/signatureService.js +26 -0
- package/lib/module/crypto/signatureService.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/GroupedSection.js +1 -1
- package/lib/module/ui/components/GroupedSection.js.map +1 -1
- package/lib/module/ui/components/OxyProvider.js +72 -25
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/components/profile/EditDisplayNameModal.js +1 -4
- package/lib/module/ui/components/profile/EditDisplayNameModal.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +176 -4
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +148 -49
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/context/hooks/useSessionManagement.js +22 -2
- package/lib/module/ui/context/hooks/useSessionManagement.js.map +1 -1
- package/lib/module/ui/hooks/mutations/index.js +6 -0
- package/lib/module/ui/hooks/mutations/index.js.map +1 -0
- package/lib/module/ui/hooks/mutations/useAccountMutations.js +308 -0
- package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -0
- package/lib/module/ui/hooks/mutations/useServicesMutations.js +185 -0
- package/lib/module/ui/hooks/mutations/useServicesMutations.js.map +1 -0
- package/lib/module/ui/hooks/queries/index.js +7 -0
- package/lib/module/ui/hooks/queries/index.js.map +1 -0
- package/lib/module/ui/hooks/queries/queryKeys.js +78 -0
- package/lib/module/ui/hooks/queries/queryKeys.js.map +1 -0
- package/lib/module/ui/hooks/queries/useAccountQueries.js +136 -0
- package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -0
- package/lib/module/ui/hooks/queries/useServicesQueries.js +130 -0
- package/lib/module/ui/hooks/queries/useServicesQueries.js.map +1 -0
- package/lib/module/ui/hooks/queryClient.js +110 -0
- package/lib/module/ui/hooks/queryClient.js.map +1 -0
- package/lib/module/ui/hooks/useIdentityMutations.js +105 -0
- package/lib/module/ui/hooks/useIdentityMutations.js.map +1 -0
- package/lib/module/ui/hooks/useProfileEditing.js +43 -59
- package/lib/module/ui/hooks/useProfileEditing.js.map +1 -1
- package/lib/module/ui/hooks/useQueryClient.js +15 -0
- package/lib/module/ui/hooks/useQueryClient.js.map +1 -0
- package/lib/module/ui/hooks/useSessionManagement.js +22 -2
- package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
- package/lib/module/ui/screens/AccountOverviewScreen.js +43 -42
- package/lib/module/ui/screens/AccountOverviewScreen.js.map +1 -1
- package/lib/module/ui/screens/AccountSettingsScreen.js +63 -58
- package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
- package/lib/module/ui/screens/WelcomeNewUserScreen.js +6 -6
- package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -1
- package/lib/module/ui/stores/accountStore.js +57 -42
- package/lib/module/ui/stores/accountStore.js.map +1 -1
- package/lib/module/ui/stores/authStore.js +4 -25
- package/lib/module/ui/stores/authStore.js.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.assets.d.ts +7 -1
- package/lib/typescript/core/mixins/OxyServices.assets.d.ts.map +1 -1
- package/lib/typescript/core/mixins/OxyServices.user.d.ts +4 -5
- package/lib/typescript/core/mixins/OxyServices.user.d.ts.map +1 -1
- package/lib/typescript/core/mixins/index.d.ts +1 -1
- package/lib/typescript/core/mixins/index.d.ts.map +1 -1
- package/lib/typescript/crypto/keyManager.d.ts +19 -2
- package/lib/typescript/crypto/keyManager.d.ts.map +1 -1
- package/lib/typescript/crypto/signatureService.d.ts +5 -0
- package/lib/typescript/crypto/signatureService.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +1 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/models/interfaces.d.ts +21 -0
- package/lib/typescript/models/interfaces.d.ts.map +1 -1
- package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/ui/components/profile/EditDisplayNameModal.d.ts.map +1 -1
- package/lib/typescript/ui/context/OxyContext.d.ts +4 -0
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/ui/context/hooks/useSessionManagement.d.ts +3 -1
- package/lib/typescript/ui/context/hooks/useSessionManagement.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/mutations/index.d.ts +3 -0
- package/lib/typescript/ui/hooks/mutations/index.d.ts.map +1 -0
- package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts +25 -0
- package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -0
- package/lib/typescript/ui/hooks/mutations/useServicesMutations.d.ts +23 -0
- package/lib/typescript/ui/hooks/mutations/useServicesMutations.d.ts.map +1 -0
- package/lib/typescript/ui/hooks/queries/index.d.ts +4 -0
- package/lib/typescript/ui/hooks/queries/index.d.ts.map +1 -0
- package/lib/typescript/ui/hooks/queries/queryKeys.d.ts +56 -0
- package/lib/typescript/ui/hooks/queries/queryKeys.d.ts.map +1 -0
- package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts +41 -0
- package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts.map +1 -0
- package/lib/typescript/ui/hooks/queries/useServicesQueries.d.ts +34 -0
- package/lib/typescript/ui/hooks/queries/useServicesQueries.d.ts.map +1 -0
- package/lib/typescript/ui/hooks/queryClient.d.ts +19 -0
- package/lib/typescript/ui/hooks/queryClient.d.ts.map +1 -0
- package/lib/typescript/ui/hooks/useIdentityMutations.d.ts +29 -0
- package/lib/typescript/ui/hooks/useIdentityMutations.d.ts.map +1 -0
- package/lib/typescript/ui/hooks/useProfileEditing.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/useQueryClient.d.ts +7 -0
- package/lib/typescript/ui/hooks/useQueryClient.d.ts.map +1 -0
- package/lib/typescript/ui/hooks/useSessionManagement.d.ts +3 -1
- package/lib/typescript/ui/hooks/useSessionManagement.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountOverviewScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
- package/lib/typescript/ui/stores/accountStore.d.ts.map +1 -1
- package/lib/typescript/ui/stores/authStore.d.ts +0 -4
- package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
- package/package.json +6 -5
- package/src/core/mixins/OxyServices.assets.ts +16 -1
- package/src/core/mixins/OxyServices.user.ts +17 -10
- package/src/crypto/keyManager.ts +177 -2
- package/src/crypto/signatureService.ts +30 -0
- package/src/index.ts +4 -1
- package/src/models/interfaces.ts +23 -0
- package/src/ui/components/GroupedSection.tsx +1 -1
- package/src/ui/components/OxyProvider.tsx +91 -37
- package/src/ui/components/profile/EditDisplayNameModal.tsx +1 -3
- package/src/ui/context/OxyContext.tsx +185 -2
- package/src/ui/context/hooks/useAuthOperations.ts +171 -58
- package/src/ui/context/hooks/useSessionManagement.ts +24 -1
- package/src/ui/hooks/mutations/index.ts +4 -0
- package/src/ui/hooks/mutations/useAccountMutations.ts +277 -0
- package/src/ui/hooks/mutations/useServicesMutations.ts +164 -0
- package/src/ui/hooks/queries/index.ts +5 -0
- package/src/ui/hooks/queries/queryKeys.ts +73 -0
- package/src/ui/hooks/queries/useAccountQueries.ts +126 -0
- package/src/ui/hooks/queries/useServicesQueries.ts +121 -0
- package/src/ui/hooks/queryClient.ts +112 -0
- package/src/ui/hooks/useIdentityMutations.ts +115 -0
- package/src/ui/hooks/useProfileEditing.ts +46 -60
- package/src/ui/hooks/useQueryClient.ts +17 -0
- package/src/ui/hooks/useSessionManagement.ts +24 -1
- package/src/ui/screens/AccountOverviewScreen.tsx +38 -46
- package/src/ui/screens/AccountSettingsScreen.tsx +54 -54
- package/src/ui/screens/WelcomeNewUserScreen.tsx +13 -12
- package/src/ui/stores/accountStore.ts +54 -43
- package/src/ui/stores/authStore.ts +3 -17
|
@@ -85,7 +85,7 @@ export const useAuthOperations = ({
|
|
|
85
85
|
}: UseAuthOperationsOptions): UseAuthOperationsResult => {
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
|
-
* Internal function to perform challenge-response sign in
|
|
88
|
+
* Internal function to perform challenge-response sign in (works offline)
|
|
89
89
|
*/
|
|
90
90
|
const performSignIn = useCallback(
|
|
91
91
|
async (publicKey: string): Promise<User> => {
|
|
@@ -94,8 +94,36 @@ export const useAuthOperations = ({
|
|
|
94
94
|
const deviceInfo = await DeviceManager.getDeviceInfo();
|
|
95
95
|
const deviceName = deviceInfo.deviceName || DeviceManager.getDefaultDeviceName();
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
let challenge: string;
|
|
98
|
+
let isOffline = false;
|
|
99
|
+
|
|
100
|
+
// Try to request challenge from server (online)
|
|
101
|
+
try {
|
|
102
|
+
const challengeResponse = await oxyServices.requestChallenge(publicKey);
|
|
103
|
+
challenge = challengeResponse.challenge;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
// Network error - generate challenge locally for offline sign-in
|
|
106
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
107
|
+
const isNetworkError =
|
|
108
|
+
errorMessage.includes('Network') ||
|
|
109
|
+
errorMessage.includes('network') ||
|
|
110
|
+
errorMessage.includes('Failed to fetch') ||
|
|
111
|
+
errorMessage.includes('fetch failed') ||
|
|
112
|
+
(error as any)?.code === 'NETWORK_ERROR' ||
|
|
113
|
+
(error as any)?.status === 0;
|
|
114
|
+
|
|
115
|
+
if (isNetworkError) {
|
|
116
|
+
if (__DEV__ && logger) {
|
|
117
|
+
logger('Network unavailable, performing offline sign-in');
|
|
118
|
+
}
|
|
119
|
+
// Generate challenge locally
|
|
120
|
+
challenge = await SignatureService.generateChallenge();
|
|
121
|
+
isOffline = true;
|
|
122
|
+
} else {
|
|
123
|
+
// Re-throw non-network errors
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
99
127
|
|
|
100
128
|
// Note: Biometric authentication check should be handled by the app layer
|
|
101
129
|
// (e.g., accounts app) before calling signIn. The biometric preference is stored
|
|
@@ -104,66 +132,129 @@ export const useAuthOperations = ({
|
|
|
104
132
|
// Sign the challenge
|
|
105
133
|
const { challenge: signature, timestamp } = await SignatureService.signChallenge(challenge);
|
|
106
134
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
publicKey,
|
|
110
|
-
challenge,
|
|
111
|
-
signature,
|
|
112
|
-
timestamp,
|
|
113
|
-
deviceName,
|
|
114
|
-
deviceFingerprint,
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
// Get token for the session
|
|
118
|
-
await oxyServices.getTokenBySession(sessionResponse.sessionId);
|
|
119
|
-
|
|
120
|
-
// Get full user data
|
|
121
|
-
const fullUser = await oxyServices.getUserBySession(sessionResponse.sessionId);
|
|
122
|
-
await applyLanguagePreference(fullUser);
|
|
123
|
-
loginSuccess(fullUser);
|
|
135
|
+
let fullUser: User;
|
|
136
|
+
let sessionResponse: SessionLoginResponse;
|
|
124
137
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
fallbackDeviceId: sessionResponse.deviceId,
|
|
130
|
-
fallbackUserId: fullUser.id,
|
|
131
|
-
logger,
|
|
132
|
-
});
|
|
133
|
-
} catch (error) {
|
|
134
|
-
if (__DEV__) {
|
|
135
|
-
console.warn('Failed to fetch device sessions after login:', error);
|
|
138
|
+
if (isOffline) {
|
|
139
|
+
// Offline sign-in: create local session and minimal user object
|
|
140
|
+
if (__DEV__ && logger) {
|
|
141
|
+
logger('Creating offline session');
|
|
136
142
|
}
|
|
137
|
-
}
|
|
138
143
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
// Generate a local session ID
|
|
145
|
+
const localSessionId = `offline_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
146
|
+
const localDeviceId = `device_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
147
|
+
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); // 7 days
|
|
148
|
+
|
|
149
|
+
// Create minimal user object with publicKey as id
|
|
150
|
+
fullUser = {
|
|
151
|
+
id: publicKey, // Use publicKey as id (per migration document)
|
|
152
|
+
publicKey,
|
|
153
|
+
username: '',
|
|
154
|
+
privacySettings: {},
|
|
155
|
+
} as User;
|
|
156
|
+
|
|
157
|
+
sessionResponse = {
|
|
158
|
+
sessionId: localSessionId,
|
|
159
|
+
deviceId: localDeviceId,
|
|
160
|
+
expiresAt,
|
|
161
|
+
user: {
|
|
162
|
+
id: publicKey,
|
|
163
|
+
username: '',
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Store offline session locally
|
|
168
|
+
const offlineSession: ClientSession = {
|
|
169
|
+
sessionId: localSessionId,
|
|
170
|
+
deviceId: localDeviceId,
|
|
171
|
+
expiresAt,
|
|
172
|
+
lastActive: new Date().toISOString(),
|
|
173
|
+
userId: publicKey,
|
|
174
|
+
isCurrent: true,
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
setActiveSessionId(localSessionId);
|
|
178
|
+
await saveActiveSessionId(localSessionId);
|
|
179
|
+
updateSessions([offlineSession], { merge: true });
|
|
180
|
+
|
|
181
|
+
// Mark session as offline for later sync
|
|
182
|
+
if (storage) {
|
|
183
|
+
await storage.setItem(`oxy_session_${localSessionId}_offline`, 'true');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (__DEV__ && logger) {
|
|
187
|
+
logger('Offline sign-in successful');
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
// Online sign-in: use normal flow
|
|
191
|
+
// Verify and create session
|
|
192
|
+
sessionResponse = await oxyServices.verifyChallenge(
|
|
193
|
+
publicKey,
|
|
194
|
+
challenge,
|
|
195
|
+
signature,
|
|
196
|
+
timestamp,
|
|
197
|
+
deviceName,
|
|
198
|
+
deviceFingerprint,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Get token for the session
|
|
202
|
+
await oxyServices.getTokenBySession(sessionResponse.sessionId);
|
|
145
203
|
|
|
146
|
-
|
|
147
|
-
|
|
204
|
+
// Get full user data
|
|
205
|
+
fullUser = await oxyServices.getUserBySession(sessionResponse.sessionId);
|
|
206
|
+
|
|
207
|
+
// Ensure id is set to publicKey (per migration document)
|
|
208
|
+
if (fullUser.id !== fullUser.publicKey) {
|
|
209
|
+
fullUser.id = fullUser.publicKey;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Fetch device sessions
|
|
213
|
+
let allDeviceSessions: ClientSession[] = [];
|
|
148
214
|
try {
|
|
149
|
-
await oxyServices
|
|
150
|
-
|
|
215
|
+
allDeviceSessions = await fetchSessionsWithFallback(oxyServices, sessionResponse.sessionId, {
|
|
216
|
+
fallbackDeviceId: sessionResponse.deviceId,
|
|
217
|
+
fallbackUserId: fullUser.id,
|
|
218
|
+
logger,
|
|
219
|
+
});
|
|
220
|
+
} catch (error) {
|
|
151
221
|
if (__DEV__) {
|
|
152
|
-
console.warn('Failed to
|
|
222
|
+
console.warn('Failed to fetch device sessions after login:', error);
|
|
153
223
|
}
|
|
154
224
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
225
|
+
|
|
226
|
+
// Check for existing session for same user
|
|
227
|
+
const existingSession = allDeviceSessions.find(
|
|
228
|
+
(session) =>
|
|
229
|
+
session.userId?.toString() === fullUser.id?.toString() &&
|
|
230
|
+
session.sessionId !== sessionResponse.sessionId,
|
|
159
231
|
);
|
|
160
|
-
|
|
161
|
-
|
|
232
|
+
|
|
233
|
+
if (existingSession) {
|
|
234
|
+
// Logout duplicate session
|
|
235
|
+
try {
|
|
236
|
+
await oxyServices.logoutSession(sessionResponse.sessionId, sessionResponse.sessionId);
|
|
237
|
+
} catch (logoutError) {
|
|
238
|
+
if (__DEV__) {
|
|
239
|
+
console.warn('Failed to logout duplicate session:', logoutError);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
await switchSession(existingSession.sessionId);
|
|
243
|
+
updateSessions(
|
|
244
|
+
allDeviceSessions.filter((session) => session.sessionId !== sessionResponse.sessionId),
|
|
245
|
+
{ merge: false },
|
|
246
|
+
);
|
|
247
|
+
onAuthStateChange?.(fullUser);
|
|
248
|
+
return fullUser;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
setActiveSessionId(sessionResponse.sessionId);
|
|
252
|
+
await saveActiveSessionId(sessionResponse.sessionId);
|
|
253
|
+
updateSessions(allDeviceSessions, { merge: true });
|
|
162
254
|
}
|
|
163
255
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
updateSessions(allDeviceSessions, { merge: true });
|
|
256
|
+
await applyLanguagePreference(fullUser);
|
|
257
|
+
loginSuccess(fullUser);
|
|
167
258
|
onAuthStateChange?.(fullUser);
|
|
168
259
|
|
|
169
260
|
return fullUser;
|
|
@@ -178,6 +269,7 @@ export const useAuthOperations = ({
|
|
|
178
269
|
setActiveSessionId,
|
|
179
270
|
switchSession,
|
|
180
271
|
updateSessions,
|
|
272
|
+
storage,
|
|
181
273
|
],
|
|
182
274
|
);
|
|
183
275
|
|
|
@@ -224,10 +316,27 @@ export const useAuthOperations = ({
|
|
|
224
316
|
};
|
|
225
317
|
}
|
|
226
318
|
} catch (error) {
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
319
|
+
// CRITICAL: Never delete identity on error - it may have been successfully created
|
|
320
|
+
// Only log the error and let the user recover using their recovery phrase
|
|
321
|
+
// Identity deletion should ONLY happen when explicitly requested by the user
|
|
322
|
+
if (__DEV__ && logger) {
|
|
323
|
+
logger('Error during identity creation (identity may still exist):', error);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Check if identity was actually created (keys exist)
|
|
327
|
+
const hasIdentity = await KeyManager.hasIdentity().catch(() => false);
|
|
328
|
+
if (hasIdentity) {
|
|
329
|
+
// Identity exists - don't delete it! Just mark as not synced
|
|
330
|
+
await storage.setItem('oxy_identity_synced', 'false').catch(() => {});
|
|
331
|
+
setIdentitySynced(false);
|
|
332
|
+
if (__DEV__ && logger) {
|
|
333
|
+
logger('Identity was created but sync failed - user can sync later using recovery phrase');
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
// No identity exists - this was a generation failure, safe to clean up sync flag
|
|
337
|
+
await storage.removeItem('oxy_identity_synced').catch(() => {});
|
|
338
|
+
setIdentitySynced(false);
|
|
339
|
+
}
|
|
231
340
|
|
|
232
341
|
const message = handleAuthError(error, {
|
|
233
342
|
defaultMessage: 'Failed to create identity',
|
|
@@ -258,6 +367,7 @@ export const useAuthOperations = ({
|
|
|
258
367
|
|
|
259
368
|
/**
|
|
260
369
|
* Sync local identity with server (call when online)
|
|
370
|
+
* TanStack Query handles offline mutations automatically
|
|
261
371
|
*/
|
|
262
372
|
const syncIdentity = useCallback(
|
|
263
373
|
async (): Promise<User> => {
|
|
@@ -275,7 +385,6 @@ export const useAuthOperations = ({
|
|
|
275
385
|
// Check if already synced
|
|
276
386
|
const alreadySynced = await storage.getItem('oxy_identity_synced');
|
|
277
387
|
if (alreadySynced === 'true') {
|
|
278
|
-
// Already synced, just sign in
|
|
279
388
|
setIdentitySynced(true);
|
|
280
389
|
return await performSignIn(publicKey);
|
|
281
390
|
}
|
|
@@ -294,7 +403,11 @@ export const useAuthOperations = ({
|
|
|
294
403
|
setIdentitySynced(true);
|
|
295
404
|
|
|
296
405
|
// Sign in
|
|
297
|
-
|
|
406
|
+
const user = await performSignIn(publicKey);
|
|
407
|
+
|
|
408
|
+
// TanStack Query will automatically retry any pending mutations
|
|
409
|
+
|
|
410
|
+
return user;
|
|
298
411
|
} catch (error) {
|
|
299
412
|
const message = handleAuthError(error, {
|
|
300
413
|
defaultMessage: 'Failed to sync identity',
|
|
@@ -6,6 +6,8 @@ import { fetchSessionsWithFallback, mapSessionsToClient, validateSessionBatch }
|
|
|
6
6
|
import { getStorageKeys, type StorageInterface } from '../utils/storageHelpers';
|
|
7
7
|
import { handleAuthError, isInvalidSessionError } from '../utils/errorHandlers';
|
|
8
8
|
import type { OxyServices } from '../../../core';
|
|
9
|
+
import type { QueryClient } from '@tanstack/react-query';
|
|
10
|
+
import { clearQueryCache } from '../../hooks/queryClient.js';
|
|
9
11
|
|
|
10
12
|
export interface UseSessionManagementOptions {
|
|
11
13
|
oxyServices: OxyServices;
|
|
@@ -19,6 +21,7 @@ export interface UseSessionManagementOptions {
|
|
|
19
21
|
setAuthError?: (message: string | null) => void;
|
|
20
22
|
logger?: (message: string, error?: unknown) => void;
|
|
21
23
|
setTokenReady?: (ready: boolean) => void;
|
|
24
|
+
queryClient?: QueryClient | null;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
export interface UseSessionManagementResult {
|
|
@@ -55,6 +58,7 @@ export const useSessionManagement = ({
|
|
|
55
58
|
setAuthError,
|
|
56
59
|
logger,
|
|
57
60
|
setTokenReady,
|
|
61
|
+
queryClient,
|
|
58
62
|
}: UseSessionManagementOptions): UseSessionManagementResult => {
|
|
59
63
|
const [sessions, setSessions] = useState<ClientSession[]>([]);
|
|
60
64
|
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
|
@@ -140,6 +144,8 @@ export const useSessionManagement = ({
|
|
|
140
144
|
try {
|
|
141
145
|
await storage.removeItem(storageKeys.activeSessionId);
|
|
142
146
|
await storage.removeItem(storageKeys.sessionIds);
|
|
147
|
+
// Clear identity sync state
|
|
148
|
+
await storage.removeItem('oxy_identity_synced').catch(() => {});
|
|
143
149
|
} catch (error) {
|
|
144
150
|
handleAuthError(error, {
|
|
145
151
|
defaultMessage: CLEAR_STORAGE_ERROR,
|
|
@@ -155,9 +161,26 @@ export const useSessionManagement = ({
|
|
|
155
161
|
setSessions([]);
|
|
156
162
|
setActiveSessionId(null);
|
|
157
163
|
logoutStore();
|
|
164
|
+
|
|
165
|
+
// Clear TanStack Query cache (in-memory)
|
|
166
|
+
if (queryClient) {
|
|
167
|
+
queryClient.clear();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Clear persisted query cache
|
|
171
|
+
if (storage) {
|
|
172
|
+
try {
|
|
173
|
+
await clearQueryCache(storage);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (logger) {
|
|
176
|
+
logger('Failed to clear persisted query cache', error);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
158
181
|
await clearSessionStorage();
|
|
159
182
|
onAuthStateChange?.(null);
|
|
160
|
-
}, [clearSessionStorage, logoutStore, onAuthStateChange]);
|
|
183
|
+
}, [clearSessionStorage, logoutStore, onAuthStateChange, queryClient, storage, logger]);
|
|
161
184
|
|
|
162
185
|
const activateSession = useCallback(
|
|
163
186
|
async (sessionId: string, user: User): Promise<void> => {
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import type { User } from '../../../models/interfaces';
|
|
3
|
+
import { queryKeys, invalidateAccountQueries, invalidateUserQueries } from '../queries/queryKeys';
|
|
4
|
+
import { useOxy } from '../../context/OxyContext';
|
|
5
|
+
import { toast } from '../../../lib/sonner';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Update user profile with optimistic updates and offline queue support
|
|
9
|
+
*/
|
|
10
|
+
export const useUpdateProfile = () => {
|
|
11
|
+
const { oxyServices, activeSessionId, user, syncIdentity } = useOxy();
|
|
12
|
+
const queryClient = useQueryClient();
|
|
13
|
+
|
|
14
|
+
return useMutation({
|
|
15
|
+
mutationFn: async (updates: Partial<User>) => {
|
|
16
|
+
// Ensure we have a valid token before making the request
|
|
17
|
+
if (!oxyServices.hasValidToken() && activeSessionId) {
|
|
18
|
+
try {
|
|
19
|
+
// Try to get token for the session
|
|
20
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
21
|
+
} catch (tokenError) {
|
|
22
|
+
// If getting token fails, might be an offline session - try syncing
|
|
23
|
+
const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
|
|
24
|
+
if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
|
|
25
|
+
try {
|
|
26
|
+
await syncIdentity();
|
|
27
|
+
// Retry getting token after sync
|
|
28
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
29
|
+
} catch (syncError) {
|
|
30
|
+
throw new Error('Session needs to be synced. Please try again.');
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
throw tokenError;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
return await oxyServices.updateProfile(updates);
|
|
40
|
+
} catch (error: any) {
|
|
41
|
+
const errorMessage = error?.message || '';
|
|
42
|
+
const status = error?.status || error?.response?.status;
|
|
43
|
+
|
|
44
|
+
// Handle authentication errors
|
|
45
|
+
if (status === 401 || errorMessage.includes('Authentication required') || errorMessage.includes('Invalid or missing authorization header')) {
|
|
46
|
+
// Try to sync session and get token
|
|
47
|
+
if (activeSessionId) {
|
|
48
|
+
try {
|
|
49
|
+
await syncIdentity();
|
|
50
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
51
|
+
// Retry the update after getting token
|
|
52
|
+
return await oxyServices.updateProfile(updates);
|
|
53
|
+
} catch (retryError) {
|
|
54
|
+
throw new Error('Authentication failed. Please sign in again.');
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
throw new Error('No active session. Please sign in.');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// TanStack Query will automatically retry on network errors
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
// Optimistic update
|
|
66
|
+
onMutate: async (updates) => {
|
|
67
|
+
// Cancel outgoing refetches
|
|
68
|
+
await queryClient.cancelQueries({ queryKey: queryKeys.accounts.current() });
|
|
69
|
+
|
|
70
|
+
// Snapshot previous value
|
|
71
|
+
const previousUser = queryClient.getQueryData<User>(queryKeys.accounts.current());
|
|
72
|
+
|
|
73
|
+
// Optimistically update
|
|
74
|
+
if (previousUser) {
|
|
75
|
+
queryClient.setQueryData<User>(queryKeys.accounts.current(), {
|
|
76
|
+
...previousUser,
|
|
77
|
+
...updates,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Also update profile query if sessionId is available
|
|
81
|
+
if (activeSessionId) {
|
|
82
|
+
queryClient.setQueryData<User>(queryKeys.users.profile(activeSessionId), {
|
|
83
|
+
...previousUser,
|
|
84
|
+
...updates,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { previousUser };
|
|
90
|
+
},
|
|
91
|
+
// On error, rollback
|
|
92
|
+
onError: (error, updates, context) => {
|
|
93
|
+
if (context?.previousUser) {
|
|
94
|
+
queryClient.setQueryData(queryKeys.accounts.current(), context.previousUser);
|
|
95
|
+
if (activeSessionId) {
|
|
96
|
+
queryClient.setQueryData(queryKeys.users.profile(activeSessionId), context.previousUser);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
toast.error(error instanceof Error ? error.message : 'Failed to update profile');
|
|
100
|
+
},
|
|
101
|
+
// On success, invalidate and refetch
|
|
102
|
+
onSuccess: (data) => {
|
|
103
|
+
// Update cache with server response
|
|
104
|
+
queryClient.setQueryData(queryKeys.accounts.current(), data);
|
|
105
|
+
if (activeSessionId) {
|
|
106
|
+
queryClient.setQueryData(queryKeys.users.profile(activeSessionId), data);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Invalidate related queries
|
|
110
|
+
invalidateUserQueries(queryClient);
|
|
111
|
+
},
|
|
112
|
+
// Always refetch after error or success
|
|
113
|
+
onSettled: () => {
|
|
114
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.accounts.current() });
|
|
115
|
+
if (activeSessionId) {
|
|
116
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.users.profile(activeSessionId) });
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Upload avatar with progress tracking and offline queue support
|
|
124
|
+
*/
|
|
125
|
+
export const useUploadAvatar = () => {
|
|
126
|
+
const { oxyServices, activeSessionId, syncIdentity } = useOxy();
|
|
127
|
+
const queryClient = useQueryClient();
|
|
128
|
+
|
|
129
|
+
return useMutation({
|
|
130
|
+
mutationFn: async (file: { uri: string; type?: string; name?: string; size?: number }) => {
|
|
131
|
+
// Ensure we have a valid token before making the request
|
|
132
|
+
if (!oxyServices.hasValidToken() && activeSessionId) {
|
|
133
|
+
try {
|
|
134
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
135
|
+
} catch (tokenError) {
|
|
136
|
+
const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
|
|
137
|
+
if (errorMessage.includes('AUTH_REQUIRED_OFFLINE_SESSION') || errorMessage.includes('offline')) {
|
|
138
|
+
try {
|
|
139
|
+
await syncIdentity();
|
|
140
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
141
|
+
} catch (syncError) {
|
|
142
|
+
throw new Error('Session needs to be synced. Please try again.');
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
throw tokenError;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// Upload file first
|
|
152
|
+
const uploadResult = await oxyServices.assetUpload(file as any, 'public');
|
|
153
|
+
const fileId = uploadResult?.file?.id || uploadResult?.id || uploadResult;
|
|
154
|
+
|
|
155
|
+
if (!fileId || typeof fileId !== 'string') {
|
|
156
|
+
throw new Error('Failed to get file ID from upload result');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Update profile with file ID
|
|
160
|
+
return await oxyServices.updateProfile({ avatar: fileId });
|
|
161
|
+
} catch (error: any) {
|
|
162
|
+
const errorMessage = error?.message || '';
|
|
163
|
+
const status = error?.status || error?.response?.status;
|
|
164
|
+
|
|
165
|
+
// Handle authentication errors
|
|
166
|
+
if (status === 401 || errorMessage.includes('Authentication required') || errorMessage.includes('Invalid or missing authorization header')) {
|
|
167
|
+
if (activeSessionId) {
|
|
168
|
+
try {
|
|
169
|
+
await syncIdentity();
|
|
170
|
+
await oxyServices.getTokenBySession(activeSessionId);
|
|
171
|
+
// Retry upload
|
|
172
|
+
const uploadResult = await oxyServices.assetUpload(file as any, 'public');
|
|
173
|
+
const fileId = uploadResult?.file?.id || uploadResult?.id || uploadResult;
|
|
174
|
+
if (!fileId || typeof fileId !== 'string') {
|
|
175
|
+
throw new Error('Failed to get file ID from upload result');
|
|
176
|
+
}
|
|
177
|
+
return await oxyServices.updateProfile({ avatar: fileId });
|
|
178
|
+
} catch (retryError) {
|
|
179
|
+
throw new Error('Authentication failed. Please sign in again.');
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
throw new Error('No active session. Please sign in.');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// TanStack Query will automatically retry on network errors
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
onMutate: async (file) => {
|
|
191
|
+
await queryClient.cancelQueries({ queryKey: queryKeys.accounts.current() });
|
|
192
|
+
const previousUser = queryClient.getQueryData<User>(queryKeys.accounts.current());
|
|
193
|
+
|
|
194
|
+
// Optimistically set a temporary avatar (using file URI as placeholder)
|
|
195
|
+
if (previousUser) {
|
|
196
|
+
const optimisticUser = {
|
|
197
|
+
...previousUser,
|
|
198
|
+
avatar: file.uri, // Temporary, will be replaced with fileId
|
|
199
|
+
};
|
|
200
|
+
queryClient.setQueryData<User>(queryKeys.accounts.current(), optimisticUser);
|
|
201
|
+
if (activeSessionId) {
|
|
202
|
+
queryClient.setQueryData<User>(queryKeys.users.profile(activeSessionId), optimisticUser);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return { previousUser };
|
|
207
|
+
},
|
|
208
|
+
onError: (error, file, context) => {
|
|
209
|
+
if (context?.previousUser) {
|
|
210
|
+
queryClient.setQueryData(queryKeys.accounts.current(), context.previousUser);
|
|
211
|
+
if (activeSessionId) {
|
|
212
|
+
queryClient.setQueryData(queryKeys.users.profile(activeSessionId), context.previousUser);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
toast.error(error instanceof Error ? error.message : 'Failed to upload avatar');
|
|
216
|
+
},
|
|
217
|
+
onSuccess: (data) => {
|
|
218
|
+
queryClient.setQueryData(queryKeys.accounts.current(), data);
|
|
219
|
+
if (activeSessionId) {
|
|
220
|
+
queryClient.setQueryData(queryKeys.users.profile(activeSessionId), data);
|
|
221
|
+
}
|
|
222
|
+
invalidateUserQueries(queryClient);
|
|
223
|
+
toast.success('Avatar updated successfully');
|
|
224
|
+
},
|
|
225
|
+
onSettled: () => {
|
|
226
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.accounts.current() });
|
|
227
|
+
if (activeSessionId) {
|
|
228
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.users.profile(activeSessionId) });
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Update account settings
|
|
236
|
+
*/
|
|
237
|
+
export const useUpdateAccountSettings = () => {
|
|
238
|
+
const { oxyServices, activeSessionId } = useOxy();
|
|
239
|
+
const queryClient = useQueryClient();
|
|
240
|
+
|
|
241
|
+
return useMutation({
|
|
242
|
+
mutationFn: async (settings: Record<string, any>) => {
|
|
243
|
+
return await oxyServices.updateProfile({ privacySettings: settings });
|
|
244
|
+
},
|
|
245
|
+
onMutate: async (settings) => {
|
|
246
|
+
await queryClient.cancelQueries({ queryKey: queryKeys.accounts.settings() });
|
|
247
|
+
const previousUser = queryClient.getQueryData<User>(queryKeys.accounts.current());
|
|
248
|
+
|
|
249
|
+
if (previousUser) {
|
|
250
|
+
queryClient.setQueryData<User>(queryKeys.accounts.current(), {
|
|
251
|
+
...previousUser,
|
|
252
|
+
privacySettings: {
|
|
253
|
+
...previousUser.privacySettings,
|
|
254
|
+
...settings,
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return { previousUser };
|
|
260
|
+
},
|
|
261
|
+
onError: (error, settings, context) => {
|
|
262
|
+
if (context?.previousUser) {
|
|
263
|
+
queryClient.setQueryData(queryKeys.accounts.current(), context.previousUser);
|
|
264
|
+
}
|
|
265
|
+
toast.error(error instanceof Error ? error.message : 'Failed to update settings');
|
|
266
|
+
},
|
|
267
|
+
onSuccess: (data) => {
|
|
268
|
+
queryClient.setQueryData(queryKeys.accounts.current(), data);
|
|
269
|
+
invalidateAccountQueries(queryClient);
|
|
270
|
+
toast.success('Settings updated successfully');
|
|
271
|
+
},
|
|
272
|
+
onSettled: () => {
|
|
273
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.accounts.settings() });
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
|