@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
@@ -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
+
@@ -0,0 +1,164 @@
1
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
2
+ import type { User } from '../../../models/interfaces';
3
+ import { queryKeys, invalidateSessionQueries } from '../queries/queryKeys';
4
+ import { useOxy } from '../../context/OxyContext';
5
+ import { toast } from '../../../lib/sonner';
6
+
7
+ /**
8
+ * Switch active session
9
+ */
10
+ export const useSwitchSession = () => {
11
+ const { switchSession, activeSessionId } = useOxy();
12
+ const queryClient = useQueryClient();
13
+
14
+ return useMutation({
15
+ mutationFn: async (sessionId: string) => {
16
+ return await switchSession(sessionId);
17
+ },
18
+ onSuccess: (user) => {
19
+ // Invalidate all session queries
20
+ invalidateSessionQueries(queryClient);
21
+
22
+ // Update current user query
23
+ queryClient.setQueryData(queryKeys.accounts.current(), user);
24
+
25
+ // Invalidate account queries
26
+ queryClient.invalidateQueries({ queryKey: queryKeys.accounts.all });
27
+ },
28
+ onError: (error) => {
29
+ toast.error(error instanceof Error ? error.message : 'Failed to switch session');
30
+ },
31
+ });
32
+ };
33
+
34
+ /**
35
+ * Logout from a session
36
+ */
37
+ export const useLogoutSession = () => {
38
+ const { oxyServices, activeSessionId, sessions } = useOxy();
39
+ const queryClient = useQueryClient();
40
+
41
+ return useMutation({
42
+ mutationFn: async (targetSessionId?: string) => {
43
+ if (!activeSessionId) {
44
+ throw new Error('No active session');
45
+ }
46
+
47
+ const sessionToLogout = targetSessionId || activeSessionId;
48
+ await oxyServices.logoutSession(activeSessionId, sessionToLogout);
49
+
50
+ return sessionToLogout;
51
+ },
52
+ onMutate: async (targetSessionId) => {
53
+ // Cancel outgoing queries
54
+ await queryClient.cancelQueries({ queryKey: queryKeys.sessions.all });
55
+
56
+ // Snapshot previous sessions
57
+ const previousSessions = queryClient.getQueryData(queryKeys.sessions.list());
58
+
59
+ // Optimistically remove session
60
+ if (previousSessions) {
61
+ const sessionToLogout = targetSessionId || activeSessionId;
62
+ const updatedSessions = (previousSessions as any[]).filter(
63
+ (s: any) => s.sessionId !== sessionToLogout
64
+ );
65
+ queryClient.setQueryData(queryKeys.sessions.list(), updatedSessions);
66
+ }
67
+
68
+ return { previousSessions };
69
+ },
70
+ onError: (error, targetSessionId, context) => {
71
+ // Rollback on error
72
+ if (context?.previousSessions) {
73
+ queryClient.setQueryData(queryKeys.sessions.list(), context.previousSessions);
74
+ }
75
+ toast.error(error instanceof Error ? error.message : 'Failed to logout');
76
+ },
77
+ onSuccess: () => {
78
+ // Invalidate all session queries
79
+ invalidateSessionQueries(queryClient);
80
+ },
81
+ onSettled: () => {
82
+ queryClient.invalidateQueries({ queryKey: queryKeys.sessions.all });
83
+ },
84
+ });
85
+ };
86
+
87
+ /**
88
+ * Logout from all sessions
89
+ */
90
+ export const useLogoutAll = () => {
91
+ const { oxyServices, activeSessionId, clearSessionState } = useOxy();
92
+ const queryClient = useQueryClient();
93
+
94
+ return useMutation({
95
+ mutationFn: async () => {
96
+ if (!activeSessionId) {
97
+ throw new Error('No active session');
98
+ }
99
+
100
+ await oxyServices.logoutAllSessions(activeSessionId);
101
+ await clearSessionState();
102
+ },
103
+ onSuccess: () => {
104
+ // Clear all queries
105
+ queryClient.clear();
106
+ toast.success('Logged out from all sessions');
107
+ },
108
+ onError: (error) => {
109
+ toast.error(error instanceof Error ? error.message : 'Failed to logout from all sessions');
110
+ },
111
+ });
112
+ };
113
+
114
+ /**
115
+ * Update device name
116
+ */
117
+ export const useUpdateDeviceName = () => {
118
+ const { oxyServices, activeSessionId } = useOxy();
119
+ const queryClient = useQueryClient();
120
+
121
+ return useMutation({
122
+ mutationFn: async (deviceName: string) => {
123
+ if (!activeSessionId) {
124
+ throw new Error('No active session');
125
+ }
126
+
127
+ return await oxyServices.updateDeviceName(activeSessionId, deviceName);
128
+ },
129
+ onSuccess: () => {
130
+ // Invalidate device and session queries
131
+ queryClient.invalidateQueries({ queryKey: queryKeys.devices.all });
132
+ queryClient.invalidateQueries({ queryKey: queryKeys.sessions.all });
133
+ toast.success('Device name updated');
134
+ },
135
+ onError: (error) => {
136
+ toast.error(error instanceof Error ? error.message : 'Failed to update device name');
137
+ },
138
+ });
139
+ };
140
+
141
+ /**
142
+ * Remove a device
143
+ */
144
+ export const useRemoveDevice = () => {
145
+ const { oxyServices } = useOxy();
146
+ const queryClient = useQueryClient();
147
+
148
+ return useMutation({
149
+ mutationFn: async (deviceId: string) => {
150
+ await oxyServices.removeDevice(deviceId);
151
+ return deviceId;
152
+ },
153
+ onSuccess: () => {
154
+ // Invalidate device queries
155
+ queryClient.invalidateQueries({ queryKey: queryKeys.devices.all });
156
+ queryClient.invalidateQueries({ queryKey: queryKeys.sessions.all });
157
+ toast.success('Device removed');
158
+ },
159
+ onError: (error) => {
160
+ toast.error(error instanceof Error ? error.message : 'Failed to remove device');
161
+ },
162
+ });
163
+ };
164
+
@@ -0,0 +1,5 @@
1
+ // Export all query hooks
2
+ export * from './useAccountQueries';
3
+ export * from './useServicesQueries';
4
+ export * from './queryKeys';
5
+
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Centralized query keys for TanStack Query
3
+ *
4
+ * Following best practices:
5
+ * - Use arrays for hierarchical keys
6
+ * - Include all parameters in the key
7
+ * - Use consistent naming conventions
8
+ */
9
+
10
+ export const queryKeys = {
11
+ // Account queries
12
+ accounts: {
13
+ all: ['accounts'] as const,
14
+ lists: () => [...queryKeys.accounts.all, 'list'] as const,
15
+ list: (sessionIds: string[]) => [...queryKeys.accounts.lists(), sessionIds] as const,
16
+ details: () => [...queryKeys.accounts.all, 'detail'] as const,
17
+ detail: (sessionId: string) => [...queryKeys.accounts.details(), sessionId] as const,
18
+ current: () => [...queryKeys.accounts.all, 'current'] as const,
19
+ settings: () => [...queryKeys.accounts.all, 'settings'] as const,
20
+ },
21
+
22
+ // User queries
23
+ users: {
24
+ all: ['users'] as const,
25
+ lists: () => [...queryKeys.users.all, 'list'] as const,
26
+ list: (userIds: string[]) => [...queryKeys.users.lists(), userIds] as const,
27
+ details: () => [...queryKeys.users.all, 'detail'] as const,
28
+ detail: (userId: string) => [...queryKeys.users.details(), userId] as const,
29
+ profile: (sessionId: string) => [...queryKeys.users.details(), sessionId, 'profile'] as const,
30
+ },
31
+
32
+ // Session queries
33
+ sessions: {
34
+ all: ['sessions'] as const,
35
+ lists: () => [...queryKeys.sessions.all, 'list'] as const,
36
+ list: (userId?: string) => [...queryKeys.sessions.lists(), userId] as const,
37
+ details: () => [...queryKeys.sessions.all, 'detail'] as const,
38
+ detail: (sessionId: string) => [...queryKeys.sessions.details(), sessionId] as const,
39
+ active: () => [...queryKeys.sessions.all, 'active'] as const,
40
+ device: (deviceId: string) => [...queryKeys.sessions.all, 'device', deviceId] as const,
41
+ },
42
+
43
+ // Device queries
44
+ devices: {
45
+ all: ['devices'] as const,
46
+ lists: () => [...queryKeys.devices.all, 'list'] as const,
47
+ list: (userId?: string) => [...queryKeys.devices.lists(), userId] as const,
48
+ details: () => [...queryKeys.devices.all, 'detail'] as const,
49
+ detail: (deviceId: string) => [...queryKeys.devices.details(), deviceId] as const,
50
+ },
51
+ } as const;
52
+
53
+ /**
54
+ * Helper to invalidate all account-related queries
55
+ */
56
+ export const invalidateAccountQueries = (queryClient: any) => {
57
+ queryClient.invalidateQueries({ queryKey: queryKeys.accounts.all });
58
+ };
59
+
60
+ /**
61
+ * Helper to invalidate all user-related queries
62
+ */
63
+ export const invalidateUserQueries = (queryClient: any) => {
64
+ queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
65
+ };
66
+
67
+ /**
68
+ * Helper to invalidate all session-related queries
69
+ */
70
+ export const invalidateSessionQueries = (queryClient: any) => {
71
+ queryClient.invalidateQueries({ queryKey: queryKeys.sessions.all });
72
+ };
73
+
@@ -0,0 +1,126 @@
1
+ import { useQuery, useQueries } from '@tanstack/react-query';
2
+ import type { User } from '../../../models/interfaces';
3
+ import type { OxyServices } from '../../../core';
4
+ import { queryKeys } from './queryKeys';
5
+ import { useOxy } from '../../context/OxyContext';
6
+
7
+ /**
8
+ * Get user profile by session ID
9
+ */
10
+ export const useUserProfile = (sessionId: string | null, options?: { enabled?: boolean }) => {
11
+ const { oxyServices } = useOxy();
12
+
13
+ return useQuery({
14
+ queryKey: queryKeys.users.profile(sessionId || ''),
15
+ queryFn: async () => {
16
+ if (!sessionId) {
17
+ throw new Error('Session ID is required');
18
+ }
19
+ return await oxyServices.getUserBySession(sessionId);
20
+ },
21
+ enabled: (options?.enabled !== false) && !!sessionId,
22
+ staleTime: 5 * 60 * 1000, // 5 minutes
23
+ gcTime: 30 * 60 * 1000, // 30 minutes
24
+ });
25
+ };
26
+
27
+ /**
28
+ * Get multiple user profiles by session IDs (batch query)
29
+ */
30
+ export const useUserProfiles = (sessionIds: string[], options?: { enabled?: boolean }) => {
31
+ const { oxyServices } = useOxy();
32
+
33
+ return useQueries({
34
+ queries: sessionIds.map((sessionId) => ({
35
+ queryKey: queryKeys.users.profile(sessionId),
36
+ queryFn: async () => {
37
+ const results = await oxyServices.getUsersBySessions([sessionId]);
38
+ return results[0]?.user || null;
39
+ },
40
+ enabled: (options?.enabled !== false) && !!sessionId,
41
+ staleTime: 5 * 60 * 1000,
42
+ gcTime: 30 * 60 * 1000,
43
+ })),
44
+ });
45
+ };
46
+
47
+ /**
48
+ * Get current authenticated user
49
+ */
50
+ export const useCurrentUser = (options?: { enabled?: boolean }) => {
51
+ const { oxyServices, activeSessionId, isAuthenticated } = useOxy();
52
+
53
+ return useQuery({
54
+ queryKey: queryKeys.accounts.current(),
55
+ queryFn: async () => {
56
+ if (!activeSessionId) {
57
+ throw new Error('No active session');
58
+ }
59
+ return await oxyServices.getUserBySession(activeSessionId);
60
+ },
61
+ enabled: (options?.enabled !== false) && isAuthenticated && !!activeSessionId,
62
+ staleTime: 1 * 60 * 1000, // 1 minute for current user
63
+ gcTime: 30 * 60 * 1000,
64
+ });
65
+ };
66
+
67
+ /**
68
+ * Get user by ID
69
+ */
70
+ export const useUserById = (userId: string | null, options?: { enabled?: boolean }) => {
71
+ const { oxyServices } = useOxy();
72
+
73
+ return useQuery({
74
+ queryKey: queryKeys.users.detail(userId || ''),
75
+ queryFn: async () => {
76
+ if (!userId) {
77
+ throw new Error('User ID is required');
78
+ }
79
+ return await oxyServices.getUserById(userId);
80
+ },
81
+ enabled: (options?.enabled !== false) && !!userId,
82
+ staleTime: 5 * 60 * 1000,
83
+ gcTime: 30 * 60 * 1000,
84
+ });
85
+ };
86
+
87
+ /**
88
+ * Get user profile by username
89
+ */
90
+ export const useUserByUsername = (username: string | null, options?: { enabled?: boolean }) => {
91
+ const { oxyServices } = useOxy();
92
+
93
+ return useQuery({
94
+ queryKey: [...queryKeys.users.details(), 'username', username || ''],
95
+ queryFn: async () => {
96
+ if (!username) {
97
+ throw new Error('Username is required');
98
+ }
99
+ return await oxyServices.getProfileByUsername(username);
100
+ },
101
+ enabled: (options?.enabled !== false) && !!username,
102
+ staleTime: 5 * 60 * 1000,
103
+ gcTime: 30 * 60 * 1000,
104
+ });
105
+ };
106
+
107
+ /**
108
+ * Batch get users by session IDs (optimized single API call)
109
+ */
110
+ export const useUsersBySessions = (sessionIds: string[], options?: { enabled?: boolean }) => {
111
+ const { oxyServices } = useOxy();
112
+
113
+ return useQuery({
114
+ queryKey: queryKeys.accounts.list(sessionIds),
115
+ queryFn: async () => {
116
+ if (sessionIds.length === 0) {
117
+ return [];
118
+ }
119
+ return await oxyServices.getUsersBySessions(sessionIds);
120
+ },
121
+ enabled: (options?.enabled !== false) && sessionIds.length > 0,
122
+ staleTime: 5 * 60 * 1000,
123
+ gcTime: 30 * 60 * 1000,
124
+ });
125
+ };
126
+