@oxyhq/auth 2.0.4 → 2.0.6

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 (58) hide show
  1. package/dist/cjs/.tsbuildinfo +1 -1
  2. package/dist/cjs/WebOxyProvider.js +37 -0
  3. package/dist/cjs/hooks/mutations/index.js +5 -1
  4. package/dist/cjs/hooks/mutations/useAccountMutations.js +185 -43
  5. package/dist/cjs/hooks/mutations/useAppData.js +133 -0
  6. package/dist/cjs/hooks/queries/appDataQueryKeys.js +46 -0
  7. package/dist/cjs/hooks/queries/index.js +8 -1
  8. package/dist/cjs/hooks/queries/useAppData.js +87 -0
  9. package/dist/cjs/hooks/queryClient.js +136 -92
  10. package/dist/cjs/hooks/useFileDownloadUrl.js +12 -36
  11. package/dist/cjs/hooks/useSessionSocket.js +81 -94
  12. package/dist/cjs/index.js +8 -3
  13. package/dist/cjs/utils/sessionHelpers.js +3 -1
  14. package/dist/cjs/utils/storageHelpers.js +36 -10
  15. package/dist/esm/.tsbuildinfo +1 -1
  16. package/dist/esm/WebOxyProvider.js +38 -1
  17. package/dist/esm/hooks/mutations/index.js +2 -0
  18. package/dist/esm/hooks/mutations/useAccountMutations.js +186 -44
  19. package/dist/esm/hooks/mutations/useAppData.js +128 -0
  20. package/dist/esm/hooks/queries/appDataQueryKeys.js +42 -0
  21. package/dist/esm/hooks/queries/index.js +3 -0
  22. package/dist/esm/hooks/queries/useAppData.js +82 -0
  23. package/dist/esm/hooks/queryClient.js +132 -89
  24. package/dist/esm/hooks/useFileDownloadUrl.js +11 -34
  25. package/dist/esm/hooks/useSessionSocket.js +81 -94
  26. package/dist/esm/index.js +3 -3
  27. package/dist/esm/utils/sessionHelpers.js +3 -1
  28. package/dist/esm/utils/storageHelpers.js +36 -10
  29. package/dist/types/.tsbuildinfo +1 -1
  30. package/dist/types/WebOxyProvider.d.ts +1 -1
  31. package/dist/types/hooks/mutations/index.d.ts +1 -0
  32. package/dist/types/hooks/mutations/useAccountMutations.d.ts +153 -9
  33. package/dist/types/hooks/mutations/useAppData.d.ts +47 -0
  34. package/dist/types/hooks/queries/appDataQueryKeys.d.ts +24 -0
  35. package/dist/types/hooks/queries/index.d.ts +2 -0
  36. package/dist/types/hooks/queries/useAccountQueries.d.ts +11 -7
  37. package/dist/types/hooks/queries/useAppData.d.ts +46 -0
  38. package/dist/types/hooks/queries/useSecurityQueries.d.ts +2 -2
  39. package/dist/types/hooks/queries/useServicesQueries.d.ts +7 -5
  40. package/dist/types/hooks/queryClient.d.ts +24 -10
  41. package/dist/types/hooks/useAssets.d.ts +1 -1
  42. package/dist/types/hooks/useFileDownloadUrl.d.ts +2 -6
  43. package/dist/types/index.d.ts +3 -3
  44. package/dist/types/utils/sessionHelpers.d.ts +3 -1
  45. package/package.json +22 -3
  46. package/src/WebOxyProvider.tsx +39 -1
  47. package/src/hooks/mutations/index.ts +3 -0
  48. package/src/hooks/mutations/useAccountMutations.ts +230 -57
  49. package/src/hooks/mutations/useAppData.ts +167 -0
  50. package/src/hooks/queries/appDataQueryKeys.ts +53 -0
  51. package/src/hooks/queries/index.ts +4 -0
  52. package/src/hooks/queries/useAppData.ts +105 -0
  53. package/src/hooks/queryClient.ts +140 -83
  54. package/src/hooks/useFileDownloadUrl.ts +15 -39
  55. package/src/hooks/useSessionSocket.ts +123 -91
  56. package/src/index.ts +7 -1
  57. package/src/utils/sessionHelpers.ts +3 -1
  58. package/src/utils/storageHelpers.ts +49 -10
@@ -40,6 +40,29 @@ function WebOxyProvider({ children, baseURL, authWebUrl, onAuthStateChange, onEr
40
40
  const [crossDomainAuth] = (0, react_1.useState)(() => new core_1.CrossDomainAuth(oxyServices));
41
41
  const [authManager] = (0, react_1.useState)(() => (0, core_1.createAuthManager)(oxyServices, { autoRefresh: true }));
42
42
  const [queryClient] = (0, react_1.useState)(() => (0, queryClient_1.createQueryClient)());
43
+ // Block first render until the persisted localStorage cache has been
44
+ // restored — mirrors the RN OxyProvider pattern. Without this gate the
45
+ // first paint observes an empty cache and any consumer reading
46
+ // `getQueryData(...)` synchronously (or using `placeholderData: 'previous'`
47
+ // gating) misses the persisted blob.
48
+ //
49
+ // Persistence is attached inside the same effect so we can hold a
50
+ // reference to the `restored` promise and only flip `isRestoring` to
51
+ // false once it settles (success OR failure). Detach on unmount so HMR
52
+ // doesn't leak subscriptions.
53
+ const [isRestoring, setIsRestoring] = (0, react_1.useState)(true);
54
+ (0, react_1.useEffect)(() => {
55
+ let mounted = true;
56
+ const { restored, unsubscribe } = (0, queryClient_1.attachQueryPersistence)(queryClient);
57
+ restored.finally(() => {
58
+ if (mounted)
59
+ setIsRestoring(false);
60
+ });
61
+ return () => {
62
+ mounted = false;
63
+ unsubscribe();
64
+ };
65
+ }, [queryClient]);
43
66
  // Auth state
44
67
  const [user, setUser] = (0, react_1.useState)(null);
45
68
  const [isLoading, setIsLoading] = (0, react_1.useState)(!skipAutoCheck);
@@ -258,6 +281,20 @@ function WebOxyProvider({ children, baseURL, authWebUrl, onAuthStateChange, onEr
258
281
  signIn, signInWithFedCM, signInWithPopup, signInWithRedirect,
259
282
  signOut, isFedCMSupported, switchSession, clearSessionState,
260
283
  ]);
284
+ // Mirror the RN OxyProvider pattern: don't expose the QueryClient (or
285
+ // mount children) until the persisted cache has been restored. On the
286
+ // web this prevents the first paint from observing an empty
287
+ // localStorage-backed cache, which would otherwise force every
288
+ // identity/session/auth query to refetch from the network even when a
289
+ // fresh blob was available on disk.
290
+ //
291
+ // The restored promise is wired with `.finally(...)` upstream, so this
292
+ // unblocks on both success and failure within typically <50ms (sync
293
+ // localStorage read + JSON.parse). A safety net is unnecessary: the
294
+ // restore promise always settles synchronously after one microtask.
295
+ if (isRestoring) {
296
+ return null;
297
+ }
261
298
  return ((0, jsx_runtime_1.jsx)(react_query_1.QueryClientProvider, { client: queryClient, children: (0, jsx_runtime_1.jsx)(WebOxyContext.Provider, { value: contextValue, children: children }) }));
262
299
  }
263
300
  /**
@@ -6,7 +6,7 @@
6
6
  * All mutations handle authentication, error handling, and query invalidation.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.useRemoveDevice = exports.useUpdateDeviceName = exports.useLogoutAll = exports.useLogoutSession = exports.useSwitchSession = exports.useUploadFile = exports.useUpdatePrivacySettings = exports.useUpdateAccountSettings = exports.useUploadAvatar = exports.useUpdateProfile = void 0;
9
+ exports.useDeleteAppData = exports.useSetAppData = exports.useRemoveDevice = exports.useUpdateDeviceName = exports.useLogoutAll = exports.useLogoutSession = exports.useSwitchSession = exports.useUploadFile = exports.useUpdatePrivacySettings = exports.useUpdateAccountSettings = exports.useUploadAvatar = exports.useUpdateProfile = void 0;
10
10
  // Account mutation hooks
11
11
  var useAccountMutations_1 = require("./useAccountMutations");
12
12
  Object.defineProperty(exports, "useUpdateProfile", { enumerable: true, get: function () { return useAccountMutations_1.useUpdateProfile; } });
@@ -21,3 +21,7 @@ Object.defineProperty(exports, "useLogoutSession", { enumerable: true, get: func
21
21
  Object.defineProperty(exports, "useLogoutAll", { enumerable: true, get: function () { return useServicesMutations_1.useLogoutAll; } });
22
22
  Object.defineProperty(exports, "useUpdateDeviceName", { enumerable: true, get: function () { return useServicesMutations_1.useUpdateDeviceName; } });
23
23
  Object.defineProperty(exports, "useRemoveDevice", { enumerable: true, get: function () { return useServicesMutations_1.useRemoveDevice; } });
24
+ // App-data KV store mutation hooks
25
+ var useAppData_1 = require("./useAppData");
26
+ Object.defineProperty(exports, "useSetAppData", { enumerable: true, get: function () { return useAppData_1.useSetAppData; } });
27
+ Object.defineProperty(exports, "useDeleteAppData", { enumerable: true, get: function () { return useAppData_1.useDeleteAppData; } });
@@ -12,7 +12,7 @@ const authStore_1 = require("../../stores/authStore");
12
12
  * Update user profile with optimistic updates and offline queue support
13
13
  */
14
14
  const useUpdateProfile = () => {
15
- const { oxyServices, activeSessionId, user } = (0, WebOxyProvider_1.useWebOxy)();
15
+ const { oxyServices, activeSessionId } = (0, WebOxyProvider_1.useWebOxy)();
16
16
  const queryClient = (0, react_query_1.useQueryClient)();
17
17
  return (0, react_query_1.useMutation)({
18
18
  mutationFn: async (updates) => {
@@ -40,12 +40,30 @@ const useUpdateProfile = () => {
40
40
  }
41
41
  return { previousUser };
42
42
  },
43
- // On error, rollback
43
+ // On error, rollback ONLY the keys this mutation tried to change
44
44
  onError: (error, updates, context) => {
45
- if (context?.previousUser) {
46
- queryClient.setQueryData(queryKeys_1.queryKeys.accounts.current(), context.previousUser);
45
+ if (context?.previousUser && updates) {
46
+ const previousUser = context.previousUser;
47
+ const changedKeys = Object.keys(updates);
48
+ const partialRollback = changedKeys.reduce((acc, key) => {
49
+ acc[key] = previousUser[key];
50
+ return acc;
51
+ }, {});
52
+ const current = queryClient.getQueryData(queryKeys_1.queryKeys.accounts.current());
53
+ if (current) {
54
+ queryClient.setQueryData(queryKeys_1.queryKeys.accounts.current(), {
55
+ ...current,
56
+ ...partialRollback,
57
+ });
58
+ }
47
59
  if (activeSessionId) {
48
- queryClient.setQueryData(queryKeys_1.queryKeys.users.profile(activeSessionId), context.previousUser);
60
+ const currentProfile = queryClient.getQueryData(queryKeys_1.queryKeys.users.profile(activeSessionId));
61
+ if (currentProfile) {
62
+ queryClient.setQueryData(queryKeys_1.queryKeys.users.profile(activeSessionId), {
63
+ ...currentProfile,
64
+ ...partialRollback,
65
+ });
66
+ }
49
67
  }
50
68
  }
51
69
  sonner_1.toast.error(error instanceof Error ? error.message : 'Failed to update profile');
@@ -63,9 +81,13 @@ const useUpdateProfile = () => {
63
81
  if (updates.avatar && activeSessionId && oxyServices) {
64
82
  (0, avatarUtils_1.refreshAvatarInStore)(activeSessionId, updates.avatar, oxyServices);
65
83
  }
66
- // Invalidate all related queries to refresh everywhere
84
+ // Invalidate all related queries so every consumer (AccountSwitcher,
85
+ // session lists, managed accounts, etc.) refetches the fresh profile.
86
+ // Critical right after `username` is set the first time, when every
87
+ // cached "session profile" still reports the user as unnamed.
67
88
  (0, queryKeys_1.invalidateUserQueries)(queryClient);
68
89
  (0, queryKeys_1.invalidateAccountQueries)(queryClient);
90
+ (0, queryKeys_1.invalidateSessionQueries)(queryClient);
69
91
  },
70
92
  });
71
93
  };
@@ -106,11 +128,25 @@ const useUploadAvatar = () => {
106
128
  }
107
129
  return { previousUser };
108
130
  },
109
- onError: (error, file, context) => {
131
+ onError: (error, _file, context) => {
132
+ // Avatar upload only mutates the `avatar` field — restore only that key
110
133
  if (context?.previousUser) {
111
- queryClient.setQueryData(queryKeys_1.queryKeys.accounts.current(), context.previousUser);
134
+ const previousAvatar = context.previousUser.avatar;
135
+ const current = queryClient.getQueryData(queryKeys_1.queryKeys.accounts.current());
136
+ if (current) {
137
+ queryClient.setQueryData(queryKeys_1.queryKeys.accounts.current(), {
138
+ ...current,
139
+ avatar: previousAvatar,
140
+ });
141
+ }
112
142
  if (activeSessionId) {
113
- queryClient.setQueryData(queryKeys_1.queryKeys.users.profile(activeSessionId), context.previousUser);
143
+ const currentProfile = queryClient.getQueryData(queryKeys_1.queryKeys.users.profile(activeSessionId));
144
+ if (currentProfile) {
145
+ queryClient.setQueryData(queryKeys_1.queryKeys.users.profile(activeSessionId), {
146
+ ...currentProfile,
147
+ avatar: previousAvatar,
148
+ });
149
+ }
114
150
  }
115
151
  }
116
152
  sonner_1.toast.error(error instanceof Error ? error.message : 'Failed to upload avatar');
@@ -126,25 +162,47 @@ const useUploadAvatar = () => {
126
162
  if (data?.avatar && activeSessionId && oxyServices) {
127
163
  (0, avatarUtils_1.refreshAvatarInStore)(activeSessionId, data.avatar, oxyServices);
128
164
  }
129
- // Invalidate all related queries to refresh everywhere
165
+ // Invalidate all related queries to refresh everywhere, including the
166
+ // sessions cache so other-account avatars update too.
130
167
  (0, queryKeys_1.invalidateUserQueries)(queryClient);
131
168
  (0, queryKeys_1.invalidateAccountQueries)(queryClient);
169
+ (0, queryKeys_1.invalidateSessionQueries)(queryClient);
132
170
  sonner_1.toast.success('Avatar updated successfully');
133
171
  },
134
172
  });
135
173
  };
136
174
  exports.useUploadAvatar = useUploadAvatar;
137
175
  /**
138
- * Update account settings
176
+ * Update account settings (privacy preferences).
177
+ *
178
+ * Privacy settings are not part of the `PUT /users/me` allow-list; the API
179
+ * would silently drop them. Route through `updatePrivacySettings` so the
180
+ * dedicated `PATCH /privacy/:id/privacy` endpoint performs a dot-path merge
181
+ * and returns the updated `privacySettings` object.
182
+ *
183
+ * The returned object exposes the standard mutation surface PLUS a
184
+ * convenience `mutate(updates)` / `mutateAsync(updates)` that snapshots
185
+ * the current user from `useWebOxy()` at dispatch time.
139
186
  */
140
187
  const useUpdateAccountSettings = () => {
141
- const { oxyServices, activeSessionId } = (0, WebOxyProvider_1.useWebOxy)();
188
+ const { oxyServices, activeSessionId, user } = (0, WebOxyProvider_1.useWebOxy)();
142
189
  const queryClient = (0, react_query_1.useQueryClient)();
143
- return (0, react_query_1.useMutation)({
144
- mutationFn: async (settings) => {
145
- return await oxyServices.updateProfile({ privacySettings: settings });
190
+ const mutation = (0, react_query_1.useMutation)({
191
+ mutationFn: async ({ updates, currentUser }) => {
192
+ const userId = currentUser.id;
193
+ if (!userId) {
194
+ throw new Error('User ID is required to update account settings');
195
+ }
196
+ const updatedPrivacy = await (0, core_1.authenticatedApiCall)(oxyServices, activeSessionId, () => oxyServices.updatePrivacySettings(updates, userId));
197
+ // Rebuild against the dispatch-time snapshot, NOT the live cache.
198
+ // The cache may have been mutated by a sibling write between
199
+ // dispatch and settle.
200
+ return {
201
+ ...currentUser,
202
+ privacySettings: updatedPrivacy,
203
+ };
146
204
  },
147
- onMutate: async (settings) => {
205
+ onMutate: async ({ updates }) => {
148
206
  await queryClient.cancelQueries({ queryKey: queryKeys_1.queryKeys.accounts.settings() });
149
207
  const previousUser = queryClient.getQueryData(queryKeys_1.queryKeys.accounts.current());
150
208
  if (previousUser) {
@@ -152,15 +210,31 @@ const useUpdateAccountSettings = () => {
152
210
  ...previousUser,
153
211
  privacySettings: {
154
212
  ...previousUser.privacySettings,
155
- ...settings,
213
+ ...updates,
156
214
  },
157
215
  });
158
216
  }
159
217
  return { previousUser };
160
218
  },
161
- onError: (error, settings, context) => {
162
- if (context?.previousUser) {
163
- queryClient.setQueryData(queryKeys_1.queryKeys.accounts.current(), context.previousUser);
219
+ onError: (error, { updates }, context) => {
220
+ // Restore only the privacySettings keys this mutation tried to change
221
+ if (context?.previousUser && updates) {
222
+ const previousPrivacy = context.previousUser.privacySettings ?? {};
223
+ const changedKeys = Object.keys(updates);
224
+ const partialPrivacyRollback = changedKeys.reduce((acc, key) => {
225
+ acc[key] = previousPrivacy[key];
226
+ return acc;
227
+ }, {});
228
+ const current = queryClient.getQueryData(queryKeys_1.queryKeys.accounts.current());
229
+ if (current) {
230
+ queryClient.setQueryData(queryKeys_1.queryKeys.accounts.current(), {
231
+ ...current,
232
+ privacySettings: {
233
+ ...(current.privacySettings ?? {}),
234
+ ...partialPrivacyRollback,
235
+ },
236
+ });
237
+ }
164
238
  }
165
239
  sonner_1.toast.error(error instanceof Error ? error.message : 'Failed to update settings');
166
240
  },
@@ -175,6 +249,26 @@ const useUpdateAccountSettings = () => {
175
249
  queryClient.invalidateQueries({ queryKey: queryKeys_1.queryKeys.accounts.settings() });
176
250
  },
177
251
  });
252
+ // Wrap mutate/mutateAsync so call sites pass a plain settings object and
253
+ // the current user is captured at dispatch time.
254
+ return {
255
+ ...mutation,
256
+ mutate: (updates) => {
257
+ const currentUser = user ?? queryClient.getQueryData(queryKeys_1.queryKeys.accounts.current());
258
+ if (!currentUser) {
259
+ sonner_1.toast.error('Cannot update account settings: no current user');
260
+ return;
261
+ }
262
+ mutation.mutate({ updates, currentUser });
263
+ },
264
+ mutateAsync: async (updates) => {
265
+ const currentUser = user ?? queryClient.getQueryData(queryKeys_1.queryKeys.accounts.current());
266
+ if (!currentUser) {
267
+ throw new Error('Cannot update account settings: no current user');
268
+ }
269
+ return mutation.mutateAsync({ updates, currentUser });
270
+ },
271
+ };
178
272
  };
179
273
  exports.useUpdateAccountSettings = useUpdateAccountSettings;
180
274
  /**
@@ -221,43 +315,91 @@ const useUpdatePrivacySettings = () => {
221
315
  }
222
316
  return { previousPrivacySettings, previousUser };
223
317
  },
224
- // On error, rollback
225
- onError: (error, { userId }, context) => {
318
+ // On error, rollback ONLY the privacy keys this mutation tried to change.
319
+ // Restoring the entire previous object would wipe out other concurrent
320
+ // optimistic updates (e.g. user toggles two privacy switches in quick
321
+ // succession; failure on one must not revert the other).
322
+ onError: (error, { settings, userId }, context) => {
226
323
  const targetUserId = userId || user?.id;
227
- if (context?.previousPrivacySettings && targetUserId) {
228
- queryClient.setQueryData(queryKeys_1.queryKeys.privacy.settings(targetUserId), context.previousPrivacySettings);
324
+ const changedKeys = settings ? Object.keys(settings) : [];
325
+ // Rollback the privacy.settings query (partial)
326
+ if (context?.previousPrivacySettings && targetUserId && changedKeys.length > 0) {
327
+ const previousPrivacy = context.previousPrivacySettings;
328
+ const partialPrivacyRollback = changedKeys.reduce((acc, key) => {
329
+ acc[key] = previousPrivacy[key];
330
+ return acc;
331
+ }, {});
332
+ const currentPrivacy = queryClient.getQueryData(queryKeys_1.queryKeys.privacy.settings(targetUserId));
333
+ if (currentPrivacy) {
334
+ queryClient.setQueryData(queryKeys_1.queryKeys.privacy.settings(targetUserId), {
335
+ ...currentPrivacy,
336
+ ...partialPrivacyRollback,
337
+ });
338
+ }
229
339
  }
230
- if (context?.previousUser) {
231
- queryClient.setQueryData(queryKeys_1.queryKeys.accounts.current(), context.previousUser);
340
+ // Rollback the accounts.current() user.privacySettings (partial)
341
+ if (context?.previousUser && changedKeys.length > 0) {
342
+ const previousPrivacy = (context.previousUser.privacySettings ?? {});
343
+ const partialPrivacyRollback = changedKeys.reduce((acc, key) => {
344
+ acc[key] = previousPrivacy[key];
345
+ return acc;
346
+ }, {});
347
+ const current = queryClient.getQueryData(queryKeys_1.queryKeys.accounts.current());
348
+ if (current) {
349
+ queryClient.setQueryData(queryKeys_1.queryKeys.accounts.current(), {
350
+ ...current,
351
+ privacySettings: {
352
+ ...(current.privacySettings ?? {}),
353
+ ...partialPrivacyRollback,
354
+ },
355
+ });
356
+ }
357
+ }
358
+ // After partial rollback, reconcile against the server so the cache
359
+ // converges to the authoritative state for the failed keys.
360
+ if (targetUserId) {
361
+ queryClient.invalidateQueries({ queryKey: queryKeys_1.queryKeys.privacy.settings(targetUserId) });
232
362
  }
233
363
  sonner_1.toast.error(error instanceof Error ? error.message : 'Failed to update privacy settings');
234
364
  },
235
- // On success, invalidate and refetch
236
- onSuccess: (data, { userId }) => {
365
+ // On success, MERGE the server response into the cached state. Older
366
+ // API builds returned only the changed field (or wiped the privacySettings
367
+ // subdocument when handed a partial update), which would clobber every
368
+ // other toggle if we blindly replaced. Defensive merge means the UI stays
369
+ // consistent regardless of server behaviour.
370
+ //
371
+ // BOTH the privacy.settings query AND the accounts.current() user are
372
+ // gated on `targetUserId`. If it's missing (no userId param, no logged-in
373
+ // user) the optimistic update in onMutate would have early-returned too,
374
+ // so neither cache was ever touched — there's nothing to reconcile here.
375
+ onSuccess: (data, { userId, settings }) => {
237
376
  const targetUserId = userId || user?.id;
238
- if (targetUserId) {
239
- queryClient.setQueryData(queryKeys_1.queryKeys.privacy.settings(targetUserId), data);
240
- }
241
- // Also update account query if it contains privacy settings
377
+ if (!targetUserId)
378
+ return;
379
+ const incoming = (data ?? {});
380
+ const requested = (settings ?? {});
381
+ queryClient.setQueryData(queryKeys_1.queryKeys.privacy.settings(targetUserId), (previous) => ({
382
+ ...(previous ?? {}),
383
+ ...requested,
384
+ ...incoming, // server wins for fields it explicitly returned
385
+ }));
242
386
  const currentUser = queryClient.getQueryData(queryKeys_1.queryKeys.accounts.current());
243
387
  if (currentUser) {
244
388
  const updatedUser = {
245
389
  ...currentUser,
246
- privacySettings: data,
390
+ privacySettings: {
391
+ ...(currentUser.privacySettings ?? {}),
392
+ ...requested,
393
+ ...incoming,
394
+ },
247
395
  };
248
396
  queryClient.setQueryData(queryKeys_1.queryKeys.accounts.current(), updatedUser);
249
- // Update authStore so frontend components see the changes immediately
250
397
  authStore_1.useAuthStore.getState().setUser(updatedUser);
251
398
  }
252
- (0, queryKeys_1.invalidateAccountQueries)(queryClient);
253
- },
254
- // Always refetch after error or success
255
- onSettled: (data, error, { userId }) => {
256
- const targetUserId = userId || user?.id;
257
- if (targetUserId) {
258
- queryClient.invalidateQueries({ queryKey: queryKeys_1.queryKeys.privacy.settings(targetUserId) });
259
- }
260
- queryClient.invalidateQueries({ queryKey: queryKeys_1.queryKeys.accounts.current() });
399
+ // Deliberately NOT invalidating any queries here. invalidateAccountQueries
400
+ // invalidates accounts.all which is the prefix for accounts.current(),
401
+ // triggering a background refetch of useCurrentUser that would overwrite
402
+ // the merged state above. The onSuccess merge is the source of truth.
261
403
  },
262
404
  });
263
405
  };
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ /**
3
+ * App-Data Mutation Hooks
4
+ *
5
+ * Write side of the per-user JSON KV store. Both `useSetAppData` and
6
+ * `useDeleteAppData` apply optimistic updates against the two query keys
7
+ * that observe this data (`appDataQueryKeys.value` and the surrounding
8
+ * `appDataQueryKeys.namespace`) and roll back on error.
9
+ *
10
+ * When the underlying request fails because the endpoint isn't reachable
11
+ * (404 / network), the mutation still surfaces the error — write attempts
12
+ * are user-initiated and the caller may want to retry or fall back to
13
+ * local persistence. Reads are silent about missing endpoints; writes are
14
+ * not.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.useDeleteAppData = exports.useSetAppData = void 0;
18
+ const react_query_1 = require("@tanstack/react-query");
19
+ const core_1 = require("@oxyhq/core");
20
+ const WebOxyProvider_1 = require("../../WebOxyProvider");
21
+ const appDataQueryKeys_1 = require("../queries/appDataQueryKeys");
22
+ /**
23
+ * Upsert a per-user JSON value. Returns the value the server confirmed it
24
+ * stored — typically identical to the input but consumers should prefer the
25
+ * returned value (the server is the source of truth).
26
+ *
27
+ * Applies optimistic updates against both the single-value query key and
28
+ * the surrounding namespace query key, then rolls back on error.
29
+ */
30
+ const useSetAppData = () => {
31
+ const { oxyServices, activeSessionId } = (0, WebOxyProvider_1.useWebOxy)();
32
+ const queryClient = (0, react_query_1.useQueryClient)();
33
+ return (0, react_query_1.useMutation)({
34
+ mutationKey: ['appData', 'set'],
35
+ mutationFn: async ({ namespace, key, value }) => {
36
+ return (0, core_1.authenticatedApiCall)(oxyServices, activeSessionId, () => oxyServices.setAppData(namespace, key, value));
37
+ },
38
+ onMutate: async ({ namespace, key, value }) => {
39
+ const valueKey = appDataQueryKeys_1.appDataQueryKeys.value(namespace, key);
40
+ const namespaceKey = appDataQueryKeys_1.appDataQueryKeys.namespace(namespace);
41
+ await Promise.all([
42
+ queryClient.cancelQueries({ queryKey: valueKey }),
43
+ queryClient.cancelQueries({ queryKey: namespaceKey }),
44
+ ]);
45
+ const previousValue = queryClient.getQueryData(valueKey);
46
+ const previousNamespace = queryClient.getQueryData(namespaceKey);
47
+ queryClient.setQueryData(valueKey, value);
48
+ if (previousNamespace) {
49
+ queryClient.setQueryData(namespaceKey, {
50
+ ...previousNamespace,
51
+ [key]: value,
52
+ });
53
+ }
54
+ return { previousValue, previousNamespace };
55
+ },
56
+ onError: (_error, { namespace, key }, context) => {
57
+ if (!context)
58
+ return;
59
+ const valueKey = appDataQueryKeys_1.appDataQueryKeys.value(namespace, key);
60
+ const namespaceKey = appDataQueryKeys_1.appDataQueryKeys.namespace(namespace);
61
+ // Restore exactly the snapshots we captured in onMutate. Don't merge
62
+ // with whatever's currently in the cache — that could splice in writes
63
+ // from concurrent mutations and undo their state.
64
+ queryClient.setQueryData(valueKey, context.previousValue ?? null);
65
+ if (context.previousNamespace !== undefined) {
66
+ queryClient.setQueryData(namespaceKey, context.previousNamespace);
67
+ }
68
+ },
69
+ onSuccess: (data, { namespace, key }) => {
70
+ const valueKey = appDataQueryKeys_1.appDataQueryKeys.value(namespace, key);
71
+ const namespaceKey = appDataQueryKeys_1.appDataQueryKeys.namespace(namespace);
72
+ queryClient.setQueryData(valueKey, data);
73
+ const existingNamespace = queryClient.getQueryData(namespaceKey);
74
+ if (existingNamespace) {
75
+ queryClient.setQueryData(namespaceKey, {
76
+ ...existingNamespace,
77
+ [key]: data,
78
+ });
79
+ }
80
+ },
81
+ });
82
+ };
83
+ exports.useSetAppData = useSetAppData;
84
+ /**
85
+ * Delete a per-user JSON value. Optimistically removes the entry from the
86
+ * single-value cache and from the surrounding namespace map, then rolls back
87
+ * on error.
88
+ */
89
+ const useDeleteAppData = () => {
90
+ const { oxyServices, activeSessionId } = (0, WebOxyProvider_1.useWebOxy)();
91
+ const queryClient = (0, react_query_1.useQueryClient)();
92
+ return (0, react_query_1.useMutation)({
93
+ mutationKey: ['appData', 'delete'],
94
+ mutationFn: async ({ namespace, key }) => {
95
+ await (0, core_1.authenticatedApiCall)(oxyServices, activeSessionId, () => oxyServices.deleteAppData(namespace, key));
96
+ },
97
+ onMutate: async ({ namespace, key }) => {
98
+ const valueKey = appDataQueryKeys_1.appDataQueryKeys.value(namespace, key);
99
+ const namespaceKey = appDataQueryKeys_1.appDataQueryKeys.namespace(namespace);
100
+ await Promise.all([
101
+ queryClient.cancelQueries({ queryKey: valueKey }),
102
+ queryClient.cancelQueries({ queryKey: namespaceKey }),
103
+ ]);
104
+ const previousValue = queryClient.getQueryData(valueKey);
105
+ const previousNamespace = queryClient.getQueryData(namespaceKey);
106
+ queryClient.setQueryData(valueKey, null);
107
+ if (previousNamespace && key in previousNamespace) {
108
+ const next = { ...previousNamespace };
109
+ delete next[key];
110
+ queryClient.setQueryData(namespaceKey, next);
111
+ }
112
+ return { previousValue, previousNamespace };
113
+ },
114
+ onError: (_error, { namespace, key }, context) => {
115
+ if (!context)
116
+ return;
117
+ const valueKey = appDataQueryKeys_1.appDataQueryKeys.value(namespace, key);
118
+ const namespaceKey = appDataQueryKeys_1.appDataQueryKeys.namespace(namespace);
119
+ queryClient.setQueryData(valueKey, context.previousValue ?? null);
120
+ if (context.previousNamespace !== undefined) {
121
+ queryClient.setQueryData(namespaceKey, context.previousNamespace);
122
+ }
123
+ },
124
+ onSuccess: (_data, { namespace, key }) => {
125
+ queryClient.setQueryData(appDataQueryKeys_1.appDataQueryKeys.value(namespace, key), null);
126
+ // Confirm the value is gone from the namespace cache too. If the
127
+ // optimistic update wasn't applied (e.g. cache was empty), this is a
128
+ // no-op; if it was, we already removed it in onMutate, so this is also
129
+ // a no-op. The work happens in onMutate — onSuccess is the commit point.
130
+ },
131
+ });
132
+ };
133
+ exports.useDeleteAppData = useDeleteAppData;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ /**
3
+ * Query keys + error utilities for `useAppData` hooks.
4
+ *
5
+ * Lives next to the hook file so consumers can import the keys directly
6
+ * when they need to imperatively invalidate a value (e.g. after a non-React
7
+ * write through `oxyServices.setAppData`).
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.appDataQueryKeys = void 0;
11
+ exports.isMissingAppDataEndpointError = isMissingAppDataEndpointError;
12
+ exports.appDataQueryKeys = {
13
+ all: ['appData'],
14
+ namespaces: () => [...exports.appDataQueryKeys.all, 'namespace'],
15
+ namespace: (namespace) => [...exports.appDataQueryKeys.namespaces(), namespace],
16
+ values: () => [...exports.appDataQueryKeys.all, 'value'],
17
+ value: (namespace, key) => [...exports.appDataQueryKeys.values(), namespace, key],
18
+ };
19
+ /**
20
+ * True when `error` indicates the app-data endpoint isn't reachable — either
21
+ * because the API deployment doesn't have it yet (404) or there's a network
22
+ * failure with no response. We treat these as "no value stored" so consumers
23
+ * fall back to local persistence without surfacing a user-facing error.
24
+ *
25
+ * Anything else (401, 403, 500) propagates normally — those are real bugs
26
+ * the auth or retry pipeline needs to see.
27
+ */
28
+ function isMissingAppDataEndpointError(error) {
29
+ if (!error || typeof error !== 'object') {
30
+ return false;
31
+ }
32
+ const candidate = error;
33
+ const status = candidate.status ?? candidate.statusCode ?? candidate.response?.status;
34
+ // 404: endpoint not deployed on this API instance yet.
35
+ if (status === 404)
36
+ return true;
37
+ // Network errors: no response received at all. Common during local dev
38
+ // when the API server is down, or when offline.
39
+ if (candidate.code === 'NETWORK_ERROR')
40
+ return true;
41
+ const message = typeof candidate.message === 'string' ? candidate.message : '';
42
+ if (message.includes('Network Error') || message.includes('Failed to fetch')) {
43
+ return true;
44
+ }
45
+ return false;
46
+ }
@@ -6,7 +6,7 @@
6
6
  * All hooks follow the same pattern with optional `enabled` parameter.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.invalidateSessionQueries = exports.invalidateUserQueries = exports.invalidateAccountQueries = exports.queryKeys = exports.useRecentSecurityActivity = exports.useSecurityActivity = exports.useSecurityInfo = exports.useUserDevices = exports.useDeviceSessions = exports.useSession = exports.useSessions = exports.usePrivacySettings = exports.useUsersBySessions = exports.useUserByUsername = exports.useUserById = exports.useCurrentUser = exports.useUserProfiles = exports.useUserProfile = void 0;
9
+ exports.isMissingAppDataEndpointError = exports.appDataQueryKeys = exports.useAppDataNamespace = exports.useAppData = exports.invalidateSessionQueries = exports.invalidateUserQueries = exports.invalidateAccountQueries = exports.queryKeys = exports.useRecentSecurityActivity = exports.useSecurityActivity = exports.useSecurityInfo = exports.useUserDevices = exports.useDeviceSessions = exports.useSession = exports.useSessions = exports.usePrivacySettings = exports.useUsersBySessions = exports.useUserByUsername = exports.useUserById = exports.useCurrentUser = exports.useUserProfiles = exports.useUserProfile = void 0;
10
10
  // Account and user query hooks
11
11
  var useAccountQueries_1 = require("./useAccountQueries");
12
12
  Object.defineProperty(exports, "useUserProfile", { enumerable: true, get: function () { return useAccountQueries_1.useUserProfile; } });
@@ -33,3 +33,10 @@ Object.defineProperty(exports, "queryKeys", { enumerable: true, get: function ()
33
33
  Object.defineProperty(exports, "invalidateAccountQueries", { enumerable: true, get: function () { return queryKeys_1.invalidateAccountQueries; } });
34
34
  Object.defineProperty(exports, "invalidateUserQueries", { enumerable: true, get: function () { return queryKeys_1.invalidateUserQueries; } });
35
35
  Object.defineProperty(exports, "invalidateSessionQueries", { enumerable: true, get: function () { return queryKeys_1.invalidateSessionQueries; } });
36
+ // App-data KV store query hooks
37
+ var useAppData_1 = require("./useAppData");
38
+ Object.defineProperty(exports, "useAppData", { enumerable: true, get: function () { return useAppData_1.useAppData; } });
39
+ Object.defineProperty(exports, "useAppDataNamespace", { enumerable: true, get: function () { return useAppData_1.useAppDataNamespace; } });
40
+ var appDataQueryKeys_1 = require("./appDataQueryKeys");
41
+ Object.defineProperty(exports, "appDataQueryKeys", { enumerable: true, get: function () { return appDataQueryKeys_1.appDataQueryKeys; } });
42
+ Object.defineProperty(exports, "isMissingAppDataEndpointError", { enumerable: true, get: function () { return appDataQueryKeys_1.isMissingAppDataEndpointError; } });