@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.
Files changed (190) hide show
  1. package/lib/commonjs/core/mixins/OxyServices.assets.js +15 -0
  2. package/lib/commonjs/core/mixins/OxyServices.assets.js.map +1 -1
  3. package/lib/commonjs/core/mixins/OxyServices.user.js +14 -13
  4. package/lib/commonjs/core/mixins/OxyServices.user.js.map +1 -1
  5. package/lib/commonjs/crypto/keyManager.js +164 -3
  6. package/lib/commonjs/crypto/keyManager.js.map +1 -1
  7. package/lib/commonjs/crypto/signatureService.js +26 -0
  8. package/lib/commonjs/crypto/signatureService.js.map +1 -1
  9. package/lib/commonjs/index.js.map +1 -1
  10. package/lib/commonjs/ui/components/GroupedSection.js +1 -1
  11. package/lib/commonjs/ui/components/GroupedSection.js.map +1 -1
  12. package/lib/commonjs/ui/components/OxyProvider.js +71 -24
  13. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  14. package/lib/commonjs/ui/components/profile/EditDisplayNameModal.js +1 -4
  15. package/lib/commonjs/ui/components/profile/EditDisplayNameModal.js.map +1 -1
  16. package/lib/commonjs/ui/context/OxyContext.js +177 -4
  17. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  18. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +148 -49
  19. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  20. package/lib/commonjs/ui/context/hooks/useSessionManagement.js +22 -2
  21. package/lib/commonjs/ui/context/hooks/useSessionManagement.js.map +1 -1
  22. package/lib/commonjs/ui/hooks/mutations/index.js +28 -0
  23. package/lib/commonjs/ui/hooks/mutations/index.js.map +1 -0
  24. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +314 -0
  25. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -0
  26. package/lib/commonjs/ui/hooks/mutations/useServicesMutations.js +193 -0
  27. package/lib/commonjs/ui/hooks/mutations/useServicesMutations.js.map +1 -0
  28. package/lib/commonjs/ui/hooks/queries/index.js +39 -0
  29. package/lib/commonjs/ui/hooks/queries/index.js.map +1 -0
  30. package/lib/commonjs/ui/hooks/queries/queryKeys.js +85 -0
  31. package/lib/commonjs/ui/hooks/queries/queryKeys.js.map +1 -0
  32. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +145 -0
  33. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -0
  34. package/lib/commonjs/ui/hooks/queries/useServicesQueries.js +138 -0
  35. package/lib/commonjs/ui/hooks/queries/useServicesQueries.js.map +1 -0
  36. package/lib/commonjs/ui/hooks/queryClient.js +117 -0
  37. package/lib/commonjs/ui/hooks/queryClient.js.map +1 -0
  38. package/lib/commonjs/ui/hooks/useIdentityMutations.js +111 -0
  39. package/lib/commonjs/ui/hooks/useIdentityMutations.js.map +1 -0
  40. package/lib/commonjs/ui/hooks/useProfileEditing.js +42 -58
  41. package/lib/commonjs/ui/hooks/useProfileEditing.js.map +1 -1
  42. package/lib/commonjs/ui/hooks/useQueryClient.js +20 -0
  43. package/lib/commonjs/ui/hooks/useQueryClient.js.map +1 -0
  44. package/lib/commonjs/ui/hooks/useSessionManagement.js +22 -2
  45. package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
  46. package/lib/commonjs/ui/screens/AccountOverviewScreen.js +43 -42
  47. package/lib/commonjs/ui/screens/AccountOverviewScreen.js.map +1 -1
  48. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +63 -58
  49. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  50. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +6 -6
  51. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  52. package/lib/commonjs/ui/stores/accountStore.js +57 -42
  53. package/lib/commonjs/ui/stores/accountStore.js.map +1 -1
  54. package/lib/commonjs/ui/stores/authStore.js +4 -25
  55. package/lib/commonjs/ui/stores/authStore.js.map +1 -1
  56. package/lib/module/core/mixins/OxyServices.assets.js +15 -0
  57. package/lib/module/core/mixins/OxyServices.assets.js.map +1 -1
  58. package/lib/module/core/mixins/OxyServices.user.js +14 -13
  59. package/lib/module/core/mixins/OxyServices.user.js.map +1 -1
  60. package/lib/module/crypto/keyManager.js +164 -3
  61. package/lib/module/crypto/keyManager.js.map +1 -1
  62. package/lib/module/crypto/signatureService.js +26 -0
  63. package/lib/module/crypto/signatureService.js.map +1 -1
  64. package/lib/module/index.js.map +1 -1
  65. package/lib/module/ui/components/GroupedSection.js +1 -1
  66. package/lib/module/ui/components/GroupedSection.js.map +1 -1
  67. package/lib/module/ui/components/OxyProvider.js +72 -25
  68. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  69. package/lib/module/ui/components/profile/EditDisplayNameModal.js +1 -4
  70. package/lib/module/ui/components/profile/EditDisplayNameModal.js.map +1 -1
  71. package/lib/module/ui/context/OxyContext.js +176 -4
  72. package/lib/module/ui/context/OxyContext.js.map +1 -1
  73. package/lib/module/ui/context/hooks/useAuthOperations.js +148 -49
  74. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  75. package/lib/module/ui/context/hooks/useSessionManagement.js +22 -2
  76. package/lib/module/ui/context/hooks/useSessionManagement.js.map +1 -1
  77. package/lib/module/ui/hooks/mutations/index.js +6 -0
  78. package/lib/module/ui/hooks/mutations/index.js.map +1 -0
  79. package/lib/module/ui/hooks/mutations/useAccountMutations.js +308 -0
  80. package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -0
  81. package/lib/module/ui/hooks/mutations/useServicesMutations.js +185 -0
  82. package/lib/module/ui/hooks/mutations/useServicesMutations.js.map +1 -0
  83. package/lib/module/ui/hooks/queries/index.js +7 -0
  84. package/lib/module/ui/hooks/queries/index.js.map +1 -0
  85. package/lib/module/ui/hooks/queries/queryKeys.js +78 -0
  86. package/lib/module/ui/hooks/queries/queryKeys.js.map +1 -0
  87. package/lib/module/ui/hooks/queries/useAccountQueries.js +136 -0
  88. package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -0
  89. package/lib/module/ui/hooks/queries/useServicesQueries.js +130 -0
  90. package/lib/module/ui/hooks/queries/useServicesQueries.js.map +1 -0
  91. package/lib/module/ui/hooks/queryClient.js +110 -0
  92. package/lib/module/ui/hooks/queryClient.js.map +1 -0
  93. package/lib/module/ui/hooks/useIdentityMutations.js +105 -0
  94. package/lib/module/ui/hooks/useIdentityMutations.js.map +1 -0
  95. package/lib/module/ui/hooks/useProfileEditing.js +43 -59
  96. package/lib/module/ui/hooks/useProfileEditing.js.map +1 -1
  97. package/lib/module/ui/hooks/useQueryClient.js +15 -0
  98. package/lib/module/ui/hooks/useQueryClient.js.map +1 -0
  99. package/lib/module/ui/hooks/useSessionManagement.js +22 -2
  100. package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
  101. package/lib/module/ui/screens/AccountOverviewScreen.js +43 -42
  102. package/lib/module/ui/screens/AccountOverviewScreen.js.map +1 -1
  103. package/lib/module/ui/screens/AccountSettingsScreen.js +63 -58
  104. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  105. package/lib/module/ui/screens/WelcomeNewUserScreen.js +6 -6
  106. package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  107. package/lib/module/ui/stores/accountStore.js +57 -42
  108. package/lib/module/ui/stores/accountStore.js.map +1 -1
  109. package/lib/module/ui/stores/authStore.js +4 -25
  110. package/lib/module/ui/stores/authStore.js.map +1 -1
  111. package/lib/typescript/core/mixins/OxyServices.assets.d.ts +7 -1
  112. package/lib/typescript/core/mixins/OxyServices.assets.d.ts.map +1 -1
  113. package/lib/typescript/core/mixins/OxyServices.user.d.ts +4 -5
  114. package/lib/typescript/core/mixins/OxyServices.user.d.ts.map +1 -1
  115. package/lib/typescript/core/mixins/index.d.ts +1 -1
  116. package/lib/typescript/core/mixins/index.d.ts.map +1 -1
  117. package/lib/typescript/crypto/keyManager.d.ts +19 -2
  118. package/lib/typescript/crypto/keyManager.d.ts.map +1 -1
  119. package/lib/typescript/crypto/signatureService.d.ts +5 -0
  120. package/lib/typescript/crypto/signatureService.d.ts.map +1 -1
  121. package/lib/typescript/index.d.ts +1 -1
  122. package/lib/typescript/index.d.ts.map +1 -1
  123. package/lib/typescript/models/interfaces.d.ts +21 -0
  124. package/lib/typescript/models/interfaces.d.ts.map +1 -1
  125. package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
  126. package/lib/typescript/ui/components/profile/EditDisplayNameModal.d.ts.map +1 -1
  127. package/lib/typescript/ui/context/OxyContext.d.ts +4 -0
  128. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  129. package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  130. package/lib/typescript/ui/context/hooks/useSessionManagement.d.ts +3 -1
  131. package/lib/typescript/ui/context/hooks/useSessionManagement.d.ts.map +1 -1
  132. package/lib/typescript/ui/hooks/mutations/index.d.ts +3 -0
  133. package/lib/typescript/ui/hooks/mutations/index.d.ts.map +1 -0
  134. package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts +25 -0
  135. package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -0
  136. package/lib/typescript/ui/hooks/mutations/useServicesMutations.d.ts +23 -0
  137. package/lib/typescript/ui/hooks/mutations/useServicesMutations.d.ts.map +1 -0
  138. package/lib/typescript/ui/hooks/queries/index.d.ts +4 -0
  139. package/lib/typescript/ui/hooks/queries/index.d.ts.map +1 -0
  140. package/lib/typescript/ui/hooks/queries/queryKeys.d.ts +56 -0
  141. package/lib/typescript/ui/hooks/queries/queryKeys.d.ts.map +1 -0
  142. package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts +41 -0
  143. package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts.map +1 -0
  144. package/lib/typescript/ui/hooks/queries/useServicesQueries.d.ts +34 -0
  145. package/lib/typescript/ui/hooks/queries/useServicesQueries.d.ts.map +1 -0
  146. package/lib/typescript/ui/hooks/queryClient.d.ts +19 -0
  147. package/lib/typescript/ui/hooks/queryClient.d.ts.map +1 -0
  148. package/lib/typescript/ui/hooks/useIdentityMutations.d.ts +29 -0
  149. package/lib/typescript/ui/hooks/useIdentityMutations.d.ts.map +1 -0
  150. package/lib/typescript/ui/hooks/useProfileEditing.d.ts.map +1 -1
  151. package/lib/typescript/ui/hooks/useQueryClient.d.ts +7 -0
  152. package/lib/typescript/ui/hooks/useQueryClient.d.ts.map +1 -0
  153. package/lib/typescript/ui/hooks/useSessionManagement.d.ts +3 -1
  154. package/lib/typescript/ui/hooks/useSessionManagement.d.ts.map +1 -1
  155. package/lib/typescript/ui/screens/AccountOverviewScreen.d.ts.map +1 -1
  156. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  157. package/lib/typescript/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
  158. package/lib/typescript/ui/stores/accountStore.d.ts.map +1 -1
  159. package/lib/typescript/ui/stores/authStore.d.ts +0 -4
  160. package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
  161. package/package.json +6 -5
  162. package/src/core/mixins/OxyServices.assets.ts +16 -1
  163. package/src/core/mixins/OxyServices.user.ts +17 -10
  164. package/src/crypto/keyManager.ts +177 -2
  165. package/src/crypto/signatureService.ts +30 -0
  166. package/src/index.ts +4 -1
  167. package/src/models/interfaces.ts +23 -0
  168. package/src/ui/components/GroupedSection.tsx +1 -1
  169. package/src/ui/components/OxyProvider.tsx +91 -37
  170. package/src/ui/components/profile/EditDisplayNameModal.tsx +1 -3
  171. package/src/ui/context/OxyContext.tsx +185 -2
  172. package/src/ui/context/hooks/useAuthOperations.ts +171 -58
  173. package/src/ui/context/hooks/useSessionManagement.ts +24 -1
  174. package/src/ui/hooks/mutations/index.ts +4 -0
  175. package/src/ui/hooks/mutations/useAccountMutations.ts +277 -0
  176. package/src/ui/hooks/mutations/useServicesMutations.ts +164 -0
  177. package/src/ui/hooks/queries/index.ts +5 -0
  178. package/src/ui/hooks/queries/queryKeys.ts +73 -0
  179. package/src/ui/hooks/queries/useAccountQueries.ts +126 -0
  180. package/src/ui/hooks/queries/useServicesQueries.ts +121 -0
  181. package/src/ui/hooks/queryClient.ts +112 -0
  182. package/src/ui/hooks/useIdentityMutations.ts +115 -0
  183. package/src/ui/hooks/useProfileEditing.ts +46 -60
  184. package/src/ui/hooks/useQueryClient.ts +17 -0
  185. package/src/ui/hooks/useSessionManagement.ts +24 -1
  186. package/src/ui/screens/AccountOverviewScreen.tsx +38 -46
  187. package/src/ui/screens/AccountSettingsScreen.tsx +54 -54
  188. package/src/ui/screens/WelcomeNewUserScreen.tsx +13 -12
  189. package/src/ui/stores/accountStore.ts +54 -43
  190. 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
- // Request challenge
98
- const { challenge } = await oxyServices.requestChallenge(publicKey);
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
- // Verify and create session
108
- const sessionResponse = await oxyServices.verifyChallenge(
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
- // Fetch device sessions
126
- let allDeviceSessions: ClientSession[] = [];
127
- try {
128
- allDeviceSessions = await fetchSessionsWithFallback(oxyServices, sessionResponse.sessionId, {
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
- // Check for existing session for same user
140
- const existingSession = allDeviceSessions.find(
141
- (session) =>
142
- session.userId?.toString() === fullUser.id?.toString() &&
143
- session.sessionId !== sessionResponse.sessionId,
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
- if (existingSession) {
147
- // Logout duplicate session
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.logoutSession(sessionResponse.sessionId, sessionResponse.sessionId);
150
- } catch (logoutError) {
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 logout duplicate session:', logoutError);
222
+ console.warn('Failed to fetch device sessions after login:', error);
153
223
  }
154
224
  }
155
- await switchSession(existingSession.sessionId);
156
- updateSessions(
157
- allDeviceSessions.filter((session) => session.sessionId !== sessionResponse.sessionId),
158
- { merge: false },
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
- onAuthStateChange?.(fullUser);
161
- return fullUser;
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
- setActiveSessionId(sessionResponse.sessionId);
165
- await saveActiveSessionId(sessionResponse.sessionId);
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
- // Clean up identity if generation failed
228
- await KeyManager.deleteIdentity().catch(() => {});
229
- await storage.removeItem('oxy_identity_synced').catch(() => {});
230
- setIdentitySynced(true);
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
- return await performSignIn(publicKey);
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,4 @@
1
+ // Export all mutation hooks
2
+ export * from './useAccountMutations';
3
+ export * from './useServicesMutations';
4
+
@@ -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
+