@oxyhq/services 5.16.1 → 5.16.3

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 (175) hide show
  1. package/lib/commonjs/core/mixins/OxyServices.user.js +14 -13
  2. package/lib/commonjs/core/mixins/OxyServices.user.js.map +1 -1
  3. package/lib/commonjs/crypto/keyManager.js +164 -3
  4. package/lib/commonjs/crypto/keyManager.js.map +1 -1
  5. package/lib/commonjs/crypto/signatureService.js +26 -0
  6. package/lib/commonjs/crypto/signatureService.js.map +1 -1
  7. package/lib/commonjs/ui/components/GroupedSection.js +1 -1
  8. package/lib/commonjs/ui/components/GroupedSection.js.map +1 -1
  9. package/lib/commonjs/ui/components/OxyProvider.js +71 -24
  10. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  11. package/lib/commonjs/ui/components/profile/EditDisplayNameModal.js +1 -4
  12. package/lib/commonjs/ui/components/profile/EditDisplayNameModal.js.map +1 -1
  13. package/lib/commonjs/ui/context/OxyContext.js +175 -4
  14. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  15. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +148 -49
  16. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  17. package/lib/commonjs/ui/context/hooks/useSessionManagement.js +22 -2
  18. package/lib/commonjs/ui/context/hooks/useSessionManagement.js.map +1 -1
  19. package/lib/commonjs/ui/hooks/mutations/index.js +28 -0
  20. package/lib/commonjs/ui/hooks/mutations/index.js.map +1 -0
  21. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js +314 -0
  22. package/lib/commonjs/ui/hooks/mutations/useAccountMutations.js.map +1 -0
  23. package/lib/commonjs/ui/hooks/mutations/useServicesMutations.js +193 -0
  24. package/lib/commonjs/ui/hooks/mutations/useServicesMutations.js.map +1 -0
  25. package/lib/commonjs/ui/hooks/queries/index.js +39 -0
  26. package/lib/commonjs/ui/hooks/queries/index.js.map +1 -0
  27. package/lib/commonjs/ui/hooks/queries/queryKeys.js +85 -0
  28. package/lib/commonjs/ui/hooks/queries/queryKeys.js.map +1 -0
  29. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js +145 -0
  30. package/lib/commonjs/ui/hooks/queries/useAccountQueries.js.map +1 -0
  31. package/lib/commonjs/ui/hooks/queries/useServicesQueries.js +138 -0
  32. package/lib/commonjs/ui/hooks/queries/useServicesQueries.js.map +1 -0
  33. package/lib/commonjs/ui/hooks/queryClient.js +117 -0
  34. package/lib/commonjs/ui/hooks/queryClient.js.map +1 -0
  35. package/lib/commonjs/ui/hooks/useIdentityMutations.js +111 -0
  36. package/lib/commonjs/ui/hooks/useIdentityMutations.js.map +1 -0
  37. package/lib/commonjs/ui/hooks/useProfileEditing.js +42 -58
  38. package/lib/commonjs/ui/hooks/useProfileEditing.js.map +1 -1
  39. package/lib/commonjs/ui/hooks/useQueryClient.js +20 -0
  40. package/lib/commonjs/ui/hooks/useQueryClient.js.map +1 -0
  41. package/lib/commonjs/ui/hooks/useSessionManagement.js +22 -2
  42. package/lib/commonjs/ui/hooks/useSessionManagement.js.map +1 -1
  43. package/lib/commonjs/ui/screens/AccountOverviewScreen.js +43 -42
  44. package/lib/commonjs/ui/screens/AccountOverviewScreen.js.map +1 -1
  45. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +63 -58
  46. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  47. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js +6 -6
  48. package/lib/commonjs/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  49. package/lib/commonjs/ui/stores/accountStore.js +57 -42
  50. package/lib/commonjs/ui/stores/accountStore.js.map +1 -1
  51. package/lib/commonjs/ui/stores/authStore.js +4 -25
  52. package/lib/commonjs/ui/stores/authStore.js.map +1 -1
  53. package/lib/module/core/mixins/OxyServices.user.js +14 -13
  54. package/lib/module/core/mixins/OxyServices.user.js.map +1 -1
  55. package/lib/module/crypto/keyManager.js +164 -3
  56. package/lib/module/crypto/keyManager.js.map +1 -1
  57. package/lib/module/crypto/signatureService.js +26 -0
  58. package/lib/module/crypto/signatureService.js.map +1 -1
  59. package/lib/module/ui/components/GroupedSection.js +1 -1
  60. package/lib/module/ui/components/GroupedSection.js.map +1 -1
  61. package/lib/module/ui/components/OxyProvider.js +72 -25
  62. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  63. package/lib/module/ui/components/profile/EditDisplayNameModal.js +1 -4
  64. package/lib/module/ui/components/profile/EditDisplayNameModal.js.map +1 -1
  65. package/lib/module/ui/context/OxyContext.js +174 -4
  66. package/lib/module/ui/context/OxyContext.js.map +1 -1
  67. package/lib/module/ui/context/hooks/useAuthOperations.js +148 -49
  68. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  69. package/lib/module/ui/context/hooks/useSessionManagement.js +22 -2
  70. package/lib/module/ui/context/hooks/useSessionManagement.js.map +1 -1
  71. package/lib/module/ui/hooks/mutations/index.js +6 -0
  72. package/lib/module/ui/hooks/mutations/index.js.map +1 -0
  73. package/lib/module/ui/hooks/mutations/useAccountMutations.js +308 -0
  74. package/lib/module/ui/hooks/mutations/useAccountMutations.js.map +1 -0
  75. package/lib/module/ui/hooks/mutations/useServicesMutations.js +185 -0
  76. package/lib/module/ui/hooks/mutations/useServicesMutations.js.map +1 -0
  77. package/lib/module/ui/hooks/queries/index.js +7 -0
  78. package/lib/module/ui/hooks/queries/index.js.map +1 -0
  79. package/lib/module/ui/hooks/queries/queryKeys.js +78 -0
  80. package/lib/module/ui/hooks/queries/queryKeys.js.map +1 -0
  81. package/lib/module/ui/hooks/queries/useAccountQueries.js +136 -0
  82. package/lib/module/ui/hooks/queries/useAccountQueries.js.map +1 -0
  83. package/lib/module/ui/hooks/queries/useServicesQueries.js +130 -0
  84. package/lib/module/ui/hooks/queries/useServicesQueries.js.map +1 -0
  85. package/lib/module/ui/hooks/queryClient.js +110 -0
  86. package/lib/module/ui/hooks/queryClient.js.map +1 -0
  87. package/lib/module/ui/hooks/useIdentityMutations.js +105 -0
  88. package/lib/module/ui/hooks/useIdentityMutations.js.map +1 -0
  89. package/lib/module/ui/hooks/useProfileEditing.js +43 -59
  90. package/lib/module/ui/hooks/useProfileEditing.js.map +1 -1
  91. package/lib/module/ui/hooks/useQueryClient.js +15 -0
  92. package/lib/module/ui/hooks/useQueryClient.js.map +1 -0
  93. package/lib/module/ui/hooks/useSessionManagement.js +22 -2
  94. package/lib/module/ui/hooks/useSessionManagement.js.map +1 -1
  95. package/lib/module/ui/screens/AccountOverviewScreen.js +43 -42
  96. package/lib/module/ui/screens/AccountOverviewScreen.js.map +1 -1
  97. package/lib/module/ui/screens/AccountSettingsScreen.js +63 -58
  98. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  99. package/lib/module/ui/screens/WelcomeNewUserScreen.js +6 -6
  100. package/lib/module/ui/screens/WelcomeNewUserScreen.js.map +1 -1
  101. package/lib/module/ui/stores/accountStore.js +57 -42
  102. package/lib/module/ui/stores/accountStore.js.map +1 -1
  103. package/lib/module/ui/stores/authStore.js +4 -25
  104. package/lib/module/ui/stores/authStore.js.map +1 -1
  105. package/lib/typescript/core/mixins/OxyServices.user.d.ts +4 -5
  106. package/lib/typescript/core/mixins/OxyServices.user.d.ts.map +1 -1
  107. package/lib/typescript/core/mixins/index.d.ts +0 -1
  108. package/lib/typescript/core/mixins/index.d.ts.map +1 -1
  109. package/lib/typescript/crypto/keyManager.d.ts +19 -2
  110. package/lib/typescript/crypto/keyManager.d.ts.map +1 -1
  111. package/lib/typescript/crypto/signatureService.d.ts +5 -0
  112. package/lib/typescript/crypto/signatureService.d.ts.map +1 -1
  113. package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
  114. package/lib/typescript/ui/components/profile/EditDisplayNameModal.d.ts.map +1 -1
  115. package/lib/typescript/ui/context/OxyContext.d.ts +4 -0
  116. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  117. package/lib/typescript/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  118. package/lib/typescript/ui/context/hooks/useSessionManagement.d.ts +3 -1
  119. package/lib/typescript/ui/context/hooks/useSessionManagement.d.ts.map +1 -1
  120. package/lib/typescript/ui/hooks/mutations/index.d.ts +3 -0
  121. package/lib/typescript/ui/hooks/mutations/index.d.ts.map +1 -0
  122. package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts +25 -0
  123. package/lib/typescript/ui/hooks/mutations/useAccountMutations.d.ts.map +1 -0
  124. package/lib/typescript/ui/hooks/mutations/useServicesMutations.d.ts +23 -0
  125. package/lib/typescript/ui/hooks/mutations/useServicesMutations.d.ts.map +1 -0
  126. package/lib/typescript/ui/hooks/queries/index.d.ts +4 -0
  127. package/lib/typescript/ui/hooks/queries/index.d.ts.map +1 -0
  128. package/lib/typescript/ui/hooks/queries/queryKeys.d.ts +56 -0
  129. package/lib/typescript/ui/hooks/queries/queryKeys.d.ts.map +1 -0
  130. package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts +41 -0
  131. package/lib/typescript/ui/hooks/queries/useAccountQueries.d.ts.map +1 -0
  132. package/lib/typescript/ui/hooks/queries/useServicesQueries.d.ts +34 -0
  133. package/lib/typescript/ui/hooks/queries/useServicesQueries.d.ts.map +1 -0
  134. package/lib/typescript/ui/hooks/queryClient.d.ts +19 -0
  135. package/lib/typescript/ui/hooks/queryClient.d.ts.map +1 -0
  136. package/lib/typescript/ui/hooks/useIdentityMutations.d.ts +29 -0
  137. package/lib/typescript/ui/hooks/useIdentityMutations.d.ts.map +1 -0
  138. package/lib/typescript/ui/hooks/useProfileEditing.d.ts.map +1 -1
  139. package/lib/typescript/ui/hooks/useQueryClient.d.ts +7 -0
  140. package/lib/typescript/ui/hooks/useQueryClient.d.ts.map +1 -0
  141. package/lib/typescript/ui/hooks/useSessionManagement.d.ts +3 -1
  142. package/lib/typescript/ui/hooks/useSessionManagement.d.ts.map +1 -1
  143. package/lib/typescript/ui/screens/AccountOverviewScreen.d.ts.map +1 -1
  144. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  145. package/lib/typescript/ui/screens/WelcomeNewUserScreen.d.ts.map +1 -1
  146. package/lib/typescript/ui/stores/accountStore.d.ts.map +1 -1
  147. package/lib/typescript/ui/stores/authStore.d.ts +0 -4
  148. package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
  149. package/package.json +5 -4
  150. package/src/core/mixins/OxyServices.user.ts +17 -10
  151. package/src/crypto/keyManager.ts +177 -2
  152. package/src/crypto/signatureService.ts +30 -0
  153. package/src/ui/components/GroupedSection.tsx +1 -1
  154. package/src/ui/components/OxyProvider.tsx +91 -37
  155. package/src/ui/components/profile/EditDisplayNameModal.tsx +1 -3
  156. package/src/ui/context/OxyContext.tsx +185 -2
  157. package/src/ui/context/hooks/useAuthOperations.ts +171 -58
  158. package/src/ui/context/hooks/useSessionManagement.ts +24 -1
  159. package/src/ui/hooks/mutations/index.ts +4 -0
  160. package/src/ui/hooks/mutations/useAccountMutations.ts +277 -0
  161. package/src/ui/hooks/mutations/useServicesMutations.ts +164 -0
  162. package/src/ui/hooks/queries/index.ts +5 -0
  163. package/src/ui/hooks/queries/queryKeys.ts +73 -0
  164. package/src/ui/hooks/queries/useAccountQueries.ts +126 -0
  165. package/src/ui/hooks/queries/useServicesQueries.ts +121 -0
  166. package/src/ui/hooks/queryClient.ts +112 -0
  167. package/src/ui/hooks/useIdentityMutations.ts +115 -0
  168. package/src/ui/hooks/useProfileEditing.ts +46 -60
  169. package/src/ui/hooks/useQueryClient.ts +17 -0
  170. package/src/ui/hooks/useSessionManagement.ts +24 -1
  171. package/src/ui/screens/AccountOverviewScreen.tsx +38 -46
  172. package/src/ui/screens/AccountSettingsScreen.tsx +54 -54
  173. package/src/ui/screens/WelcomeNewUserScreen.tsx +13 -12
  174. package/src/ui/stores/accountStore.ts +54 -43
  175. package/src/ui/stores/authStore.ts +3 -17
@@ -26,6 +26,10 @@ import { getStorageKeys } from '../utils/storageHelpers';
26
26
  import { isInvalidSessionError } from '../utils/errorHandlers';
27
27
  import type { RouteName } from '../navigation/routes';
28
28
  import { showBottomSheet as globalShowBottomSheet } from '../navigation/bottomSheetManager';
29
+ import { useQueryClient } from '@tanstack/react-query';
30
+ import { clearQueryCache } from '../hooks/queryClient';
31
+ import { useAccountStore } from '../stores/accountStore';
32
+ import { KeyManager } from '../../crypto/keyManager';
29
33
 
30
34
  export interface OxyContextState {
31
35
  user: User | null;
@@ -34,6 +38,7 @@ export interface OxyContextState {
34
38
  isAuthenticated: boolean;
35
39
  isLoading: boolean;
36
40
  isTokenReady: boolean;
41
+ isStorageReady: boolean;
37
42
  error: string | null;
38
43
  currentLanguage: string;
39
44
  currentLanguageMetadata: ReturnType<typeof useLanguageManagement>['metadata'];
@@ -48,6 +53,7 @@ export interface OxyContextState {
48
53
  getPublicKey: () => Promise<string | null>;
49
54
  isIdentitySynced: () => Promise<boolean>;
50
55
  syncIdentity: () => Promise<User>;
56
+ deleteIdentityAndClearAccount: (skipBackup?: boolean, force?: boolean, userConfirmed?: boolean) => Promise<void>;
51
57
 
52
58
  // Identity sync state (reactive, from Zustand store)
53
59
  identitySyncState: {
@@ -73,6 +79,8 @@ export interface OxyContextState {
73
79
  >;
74
80
  logoutAllDeviceSessions: () => Promise<void>;
75
81
  updateDeviceName: (deviceName: string) => Promise<void>;
82
+ clearSessionState: () => Promise<void>;
83
+ clearAllAccountData: () => Promise<void>;
76
84
  oxyServices: OxyServices;
77
85
  useFollow?: UseFollowHook;
78
86
  showBottomSheet?: (screenOrConfig: RouteName | { screen: RouteName; props?: Record<string, unknown> }) => void;
@@ -182,7 +190,55 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
182
190
 
183
191
  const storageKeys = useMemo(() => getStorageKeys(storageKeyPrefix), [storageKeyPrefix]);
184
192
 
185
- const { storage } = useStorage({ onError, logger });
193
+ const { storage, isReady: isStorageReady } = useStorage({ onError, logger });
194
+
195
+ // Identity integrity check and auto-restore on startup
196
+ useEffect(() => {
197
+ if (!storage || !isStorageReady) return;
198
+
199
+ const checkAndRestoreIdentity = async () => {
200
+ try {
201
+ const { KeyManager } = await import('../../crypto/index.js');
202
+ // Check if identity exists and verify integrity
203
+ const hasIdentity = await KeyManager.hasIdentity();
204
+ if (hasIdentity) {
205
+ const isValid = await KeyManager.verifyIdentityIntegrity();
206
+ if (!isValid) {
207
+ // Try to restore from backup
208
+ const restored = await KeyManager.restoreIdentityFromBackup();
209
+ if (restored) {
210
+ if (__DEV__) {
211
+ logger('Identity restored from backup successfully');
212
+ }
213
+ } else {
214
+ if (__DEV__) {
215
+ logger('Identity integrity check failed - user may need to restore from recovery phrase');
216
+ }
217
+ }
218
+ } else {
219
+ // Identity is valid - ensure backup is up to date
220
+ await KeyManager.backupIdentity();
221
+ }
222
+ } else {
223
+ // No identity - try to restore from backup
224
+ const restored = await KeyManager.restoreIdentityFromBackup();
225
+ if (restored && __DEV__) {
226
+ logger('Identity restored from backup on startup');
227
+ }
228
+ }
229
+ } catch (error) {
230
+ if (__DEV__) {
231
+ logger('Error during identity integrity check', error);
232
+ }
233
+ // Don't block app startup - user can recover with recovery phrase
234
+ }
235
+ };
236
+
237
+ checkAndRestoreIdentity();
238
+ }, [storage, isStorageReady, logger]);
239
+
240
+ // Offline queuing is now handled by TanStack Query mutations
241
+ // No need for custom offline queue
186
242
 
187
243
  const {
188
244
  currentLanguage,
@@ -198,6 +254,8 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
198
254
  logger,
199
255
  });
200
256
 
257
+ const queryClient = useQueryClient();
258
+
201
259
  const {
202
260
  sessions,
203
261
  activeSessionId,
@@ -220,6 +278,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
220
278
  setAuthError: (message) => setAuthState({ error: message }),
221
279
  logger,
222
280
  setTokenReady,
281
+ queryClient,
223
282
  });
224
283
 
225
284
  const {
@@ -231,7 +290,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
231
290
  hasIdentity,
232
291
  getPublicKey,
233
292
  isIdentitySynced,
234
- syncIdentity,
293
+ syncIdentity: syncIdentityBase,
235
294
  } = useAuthOperations({
236
295
  oxyServices,
237
296
  storage,
@@ -254,6 +313,123 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
254
313
  logger,
255
314
  });
256
315
 
316
+ // syncIdentity - TanStack Query handles offline mutations automatically
317
+ const syncIdentity = useCallback(async () => {
318
+ return await syncIdentityBase();
319
+ }, [syncIdentityBase]);
320
+
321
+ // Clear all account data when identity is lost (for accounts app)
322
+ // In accounts app, identity = account, so losing identity means losing everything
323
+ const clearAllAccountData = useCallback(async (): Promise<void> => {
324
+ // Clear TanStack Query cache (in-memory)
325
+ queryClient.clear();
326
+
327
+ // Clear persisted query cache
328
+ if (storage) {
329
+ try {
330
+ await clearQueryCache(storage);
331
+ } catch (error) {
332
+ if (logger) {
333
+ logger('Failed to clear persisted query cache', error);
334
+ }
335
+ }
336
+ }
337
+
338
+ // Clear session state (sessions, activeSessionId, storage)
339
+ await clearSessionState();
340
+
341
+ // Clear identity sync state from storage
342
+ if (storage) {
343
+ try {
344
+ await storage.removeItem('oxy_identity_synced');
345
+ } catch (error) {
346
+ if (logger) {
347
+ logger('Failed to clear identity sync state', error);
348
+ }
349
+ }
350
+ }
351
+
352
+ // Reset auth store identity sync state
353
+ useAuthStore.getState().setIdentitySynced(false);
354
+ useAuthStore.getState().setSyncing(false);
355
+
356
+ // Reset account store
357
+ useAccountStore.getState().reset();
358
+
359
+ // Clear HTTP service cache
360
+ oxyServices.clearCache();
361
+ }, [queryClient, storage, clearSessionState, logger, oxyServices]);
362
+
363
+ // Delete identity and clear all account data
364
+ // In accounts app, deleting identity means losing the account completely
365
+ const deleteIdentityAndClearAccount = useCallback(async (
366
+ skipBackup: boolean = false,
367
+ force: boolean = false,
368
+ userConfirmed: boolean = false
369
+ ): Promise<void> => {
370
+ // First, clear all account data
371
+ await clearAllAccountData();
372
+
373
+ // Then delete the identity keys
374
+ await KeyManager.deleteIdentity(skipBackup, force, userConfirmed);
375
+ }, [clearAllAccountData]);
376
+
377
+ // Network reconnect sync - TanStack Query automatically retries mutations on reconnect
378
+ // We only need to sync identity if it's not synced
379
+ useEffect(() => {
380
+ if (!storage) return;
381
+
382
+ let wasOffline = false;
383
+ let checkInterval: NodeJS.Timeout | null = null;
384
+
385
+ const checkNetworkAndSync = async () => {
386
+ try {
387
+ // Try a lightweight health check to see if we're online
388
+ await oxyServices.healthCheck().catch(() => {
389
+ wasOffline = true;
390
+ return;
391
+ });
392
+
393
+ // If we were offline and now we're online, sync identity if needed
394
+ if (wasOffline) {
395
+ if (__DEV__ && logger) {
396
+ logger('Network reconnected, checking identity sync...');
397
+ }
398
+
399
+ // Sync identity first (if not synced)
400
+ try {
401
+ const isSynced = await storage.getItem('oxy_identity_synced');
402
+ if (isSynced === 'false') {
403
+ await syncIdentity();
404
+ }
405
+ } catch (syncError) {
406
+ if (__DEV__ && logger) {
407
+ logger('Error syncing identity on reconnect', syncError);
408
+ }
409
+ }
410
+
411
+ // TanStack Query will automatically retry pending mutations
412
+ wasOffline = false;
413
+ }
414
+ } catch (error) {
415
+ // Network check failed - we're offline
416
+ wasOffline = true;
417
+ }
418
+ };
419
+
420
+ // Check immediately
421
+ checkNetworkAndSync();
422
+
423
+ // Check periodically (every 10 seconds when app is active)
424
+ checkInterval = setInterval(checkNetworkAndSync, 10000);
425
+
426
+ return () => {
427
+ if (checkInterval) {
428
+ clearInterval(checkInterval);
429
+ }
430
+ };
431
+ }, [oxyServices, storage, syncIdentity, logger]);
432
+
257
433
  const { getDeviceSessions, logoutAllDeviceSessions, updateDeviceName } = useDeviceManagement({
258
434
  oxyServices,
259
435
  activeSessionId,
@@ -404,6 +580,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
404
580
  isAuthenticated,
405
581
  isLoading,
406
582
  isTokenReady: tokenReady,
583
+ isStorageReady,
407
584
  error,
408
585
  currentLanguage,
409
586
  currentLanguageMetadata,
@@ -416,6 +593,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
416
593
  getPublicKey,
417
594
  isIdentitySynced,
418
595
  syncIdentity,
596
+ deleteIdentityAndClearAccount,
419
597
  identitySyncState: {
420
598
  isSynced: isIdentitySyncedStore ?? true,
421
599
  isSyncing: isSyncing ?? false,
@@ -429,6 +607,8 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
429
607
  getDeviceSessions,
430
608
  logoutAllDeviceSessions,
431
609
  updateDeviceName,
610
+ clearSessionState,
611
+ clearAllAccountData,
432
612
  oxyServices,
433
613
  useFollow: useFollowHook,
434
614
  showBottomSheet: showBottomSheetForContext,
@@ -441,6 +621,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
441
621
  getPublicKey,
442
622
  isIdentitySynced,
443
623
  syncIdentity,
624
+ deleteIdentityAndClearAccount,
444
625
  isIdentitySyncedStore,
445
626
  isSyncing,
446
627
  currentLanguage,
@@ -460,7 +641,9 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
460
641
  setLanguage,
461
642
  switchSessionForContext,
462
643
  tokenReady,
644
+ isStorageReady,
463
645
  updateDeviceName,
646
+ clearAllAccountData,
464
647
  useFollowHook,
465
648
  user,
466
649
  showBottomSheetForContext,
@@ -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
+