@oxyhq/auth 2.0.4 → 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/WebOxyProvider.js +37 -0
- package/dist/cjs/hooks/mutations/useAccountMutations.js +185 -43
- package/dist/cjs/hooks/queryClient.js +136 -92
- package/dist/cjs/hooks/useFileDownloadUrl.js +12 -36
- package/dist/cjs/hooks/useSessionSocket.js +81 -94
- package/dist/cjs/index.js +1 -2
- package/dist/cjs/utils/sessionHelpers.js +3 -1
- package/dist/cjs/utils/storageHelpers.js +36 -10
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/WebOxyProvider.js +38 -1
- package/dist/esm/hooks/mutations/useAccountMutations.js +186 -44
- package/dist/esm/hooks/queryClient.js +132 -89
- package/dist/esm/hooks/useFileDownloadUrl.js +11 -34
- package/dist/esm/hooks/useSessionSocket.js +81 -94
- package/dist/esm/index.js +1 -1
- package/dist/esm/utils/sessionHelpers.js +3 -1
- package/dist/esm/utils/storageHelpers.js +36 -10
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/WebOxyProvider.d.ts +1 -1
- package/dist/types/hooks/mutations/useAccountMutations.d.ts +153 -9
- package/dist/types/hooks/queries/useAccountQueries.d.ts +11 -7
- package/dist/types/hooks/queries/useSecurityQueries.d.ts +2 -2
- package/dist/types/hooks/queries/useServicesQueries.d.ts +7 -5
- package/dist/types/hooks/queryClient.d.ts +24 -10
- package/dist/types/hooks/useAssets.d.ts +1 -1
- package/dist/types/hooks/useFileDownloadUrl.d.ts +2 -6
- package/dist/types/index.d.ts +1 -1
- package/dist/types/utils/sessionHelpers.d.ts +3 -1
- package/package.json +22 -3
- package/src/WebOxyProvider.tsx +39 -1
- package/src/hooks/mutations/useAccountMutations.ts +230 -57
- package/src/hooks/queryClient.ts +140 -83
- package/src/hooks/useFileDownloadUrl.ts +15 -39
- package/src/hooks/useSessionSocket.ts +123 -91
- package/src/index.ts +1 -1
- package/src/utils/sessionHelpers.ts +3 -1
- 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
|
/**
|
|
@@ -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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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,
|
|
131
|
+
onError: (error, _file, context) => {
|
|
132
|
+
// Avatar upload only mutates the `avatar` field — restore only that key
|
|
110
133
|
if (context?.previousUser) {
|
|
111
|
-
|
|
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.
|
|
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
|
-
|
|
144
|
-
mutationFn: async (
|
|
145
|
-
|
|
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 (
|
|
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
|
-
...
|
|
213
|
+
...updates,
|
|
156
214
|
},
|
|
157
215
|
});
|
|
158
216
|
}
|
|
159
217
|
return { previousUser };
|
|
160
218
|
},
|
|
161
|
-
onError: (error,
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
228
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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,
|
|
236
|
-
|
|
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
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
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:
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
};
|
|
@@ -1,110 +1,154 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Web QueryClient with offline-first defaults + localStorage persistence.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the persistence behaviour in `@oxyhq/services/queryClient` so
|
|
6
|
+
* web auth apps (FedCM, popup, redirect flows) survive a page reload with
|
|
7
|
+
* cached identity + paused mutations intact.
|
|
8
|
+
*
|
|
9
|
+
* Persistence is opt-in via `attachQueryPersistence(...)` so SSR callers
|
|
10
|
+
* (Next.js getServerSideProps, Vite SSR, tests) can create a stateless
|
|
11
|
+
* client without touching `window`.
|
|
12
|
+
*/
|
|
2
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.clearQueryCache = exports.createQueryClient = exports.createPersistenceAdapter = void 0;
|
|
14
|
+
exports.clearQueryCache = exports.attachQueryPersistence = exports.createQueryClient = exports.createPersistenceAdapter = void 0;
|
|
4
15
|
const react_query_1 = require("@tanstack/react-query");
|
|
5
|
-
const
|
|
6
|
-
const
|
|
16
|
+
const react_query_persist_client_1 = require("@tanstack/react-query-persist-client");
|
|
17
|
+
const query_sync_storage_persister_1 = require("@tanstack/query-sync-storage-persister");
|
|
18
|
+
const QUERY_CACHE_KEY = 'oxy_auth_query_cache_v2';
|
|
19
|
+
const QUERY_CACHE_MAX_AGE = 30 * 24 * 60 * 60 * 1000;
|
|
20
|
+
const QUERY_PERSIST_THROTTLE_MS = 1000;
|
|
7
21
|
/**
|
|
8
|
-
*
|
|
22
|
+
* Query-key prefixes whose data is safe to restore across reloads.
|
|
23
|
+
* Web auth surfaces are session/profile heavy — lists and history are not
|
|
24
|
+
* persisted to keep the localStorage footprint small.
|
|
9
25
|
*/
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
const PERSISTED_QUERY_PREFIXES = [
|
|
27
|
+
'accounts',
|
|
28
|
+
'users',
|
|
29
|
+
'sessions',
|
|
30
|
+
'auth',
|
|
31
|
+
];
|
|
32
|
+
function shouldDehydrateQuery(query) {
|
|
33
|
+
if (query.state.status !== 'success')
|
|
34
|
+
return false;
|
|
35
|
+
const head = query.queryKey[0];
|
|
36
|
+
return typeof head === 'string' && PERSISTED_QUERY_PREFIXES.includes(head);
|
|
37
|
+
}
|
|
38
|
+
function shouldDehydrateMutation(_mutation) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Best-effort detection — works in browsers, Node SSR, and React Server
|
|
43
|
+
* Components. `localStorage` is gated behind `window` because Node and edge
|
|
44
|
+
* runtimes may polyfill `globalThis.localStorage` inconsistently.
|
|
45
|
+
*/
|
|
46
|
+
function getBrowserLocalStorage() {
|
|
47
|
+
if (typeof window === 'undefined')
|
|
48
|
+
return null;
|
|
49
|
+
try {
|
|
50
|
+
if (!window.localStorage)
|
|
51
|
+
return null;
|
|
52
|
+
return window.localStorage;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Access blocked (Safari Private Mode, sandboxed iframe, etc.)
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const createPersistenceAdapter = (storage) => ({
|
|
60
|
+
persistClient: async (client) => {
|
|
61
|
+
try {
|
|
62
|
+
await storage.setItem(QUERY_CACHE_KEY, JSON.stringify(client));
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
66
|
+
console.warn('[QueryClient] Failed to persist cache', error);
|
|
20
67
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
restoreClient: async () => {
|
|
71
|
+
try {
|
|
72
|
+
const cached = await storage.getItem(QUERY_CACHE_KEY);
|
|
73
|
+
return cached ? JSON.parse(cached) : undefined;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
77
|
+
console.warn('[QueryClient] Failed to restore cache', error);
|
|
25
78
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
await storage.removeItem(QUERY_CACHE_KEY);
|
|
37
|
-
return undefined;
|
|
38
|
-
}
|
|
39
|
-
// Check if cache is too old (30 days)
|
|
40
|
-
const maxAge = 30 * 24 * 60 * 60 * 1000;
|
|
41
|
-
if (parsed.timestamp && Date.now() - parsed.timestamp > maxAge) {
|
|
42
|
-
await storage.removeItem(QUERY_CACHE_KEY);
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
return parsed.clientState;
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
49
|
-
console.warn('[QueryClient] Failed to restore cache:', error);
|
|
50
|
-
}
|
|
51
|
-
return undefined;
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
removeClient: async () => {
|
|
83
|
+
try {
|
|
84
|
+
await storage.removeItem(QUERY_CACHE_KEY);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
88
|
+
console.warn('[QueryClient] Failed to remove cache', error);
|
|
52
89
|
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
exports.createPersistenceAdapter = createPersistenceAdapter;
|
|
94
|
+
const createQueryClient = () => new react_query_1.QueryClient({
|
|
95
|
+
defaultOptions: {
|
|
96
|
+
queries: {
|
|
97
|
+
staleTime: 5 * 60 * 1000,
|
|
98
|
+
gcTime: 30 * 60 * 1000,
|
|
99
|
+
retry: 3,
|
|
100
|
+
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000),
|
|
101
|
+
refetchOnReconnect: true,
|
|
102
|
+
refetchOnWindowFocus: false,
|
|
103
|
+
networkMode: 'offlineFirst',
|
|
53
104
|
},
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
60
|
-
console.warn('[QueryClient] Failed to remove cache:', error);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
105
|
+
mutations: {
|
|
106
|
+
retry: 1,
|
|
107
|
+
networkMode: 'offlineFirst',
|
|
63
108
|
},
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
exports.
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
exports.createQueryClient = createQueryClient;
|
|
67
112
|
/**
|
|
68
|
-
*
|
|
113
|
+
* Wire `persistQueryClient` to browser `localStorage` (or a no-op when not
|
|
114
|
+
* in a browser). Returns the restore promise so consumers can `await` it
|
|
115
|
+
* before exposing the client to <Suspense> boundaries.
|
|
69
116
|
*/
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
},
|
|
88
|
-
mutations: {
|
|
89
|
-
// Retry once for mutations
|
|
90
|
-
retry: 1,
|
|
91
|
-
// Offline-first: queue mutations when offline
|
|
92
|
-
networkMode: 'offlineFirst',
|
|
93
|
-
},
|
|
117
|
+
const attachQueryPersistence = (queryClient) => {
|
|
118
|
+
const localStorage = getBrowserLocalStorage();
|
|
119
|
+
if (!localStorage) {
|
|
120
|
+
return { restored: Promise.resolve(), unsubscribe: () => { } };
|
|
121
|
+
}
|
|
122
|
+
const persister = (0, query_sync_storage_persister_1.createSyncStoragePersister)({
|
|
123
|
+
storage: localStorage,
|
|
124
|
+
key: QUERY_CACHE_KEY,
|
|
125
|
+
throttleTime: QUERY_PERSIST_THROTTLE_MS,
|
|
126
|
+
});
|
|
127
|
+
const [unsubscribe, restored] = (0, react_query_persist_client_1.persistQueryClient)({
|
|
128
|
+
queryClient,
|
|
129
|
+
persister,
|
|
130
|
+
maxAge: QUERY_CACHE_MAX_AGE,
|
|
131
|
+
dehydrateOptions: {
|
|
132
|
+
shouldDehydrateQuery,
|
|
133
|
+
shouldDehydrateMutation,
|
|
94
134
|
},
|
|
95
135
|
});
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
136
|
+
restored.catch((error) => {
|
|
137
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
138
|
+
console.warn('[QueryClient] Failed to restore persisted cache', error);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
return { unsubscribe, restored };
|
|
101
142
|
};
|
|
102
|
-
exports.
|
|
103
|
-
/**
|
|
104
|
-
* Clear persisted query cache
|
|
105
|
-
*/
|
|
143
|
+
exports.attachQueryPersistence = attachQueryPersistence;
|
|
106
144
|
const clearQueryCache = async (storage) => {
|
|
107
|
-
|
|
108
|
-
|
|
145
|
+
try {
|
|
146
|
+
await storage.removeItem(QUERY_CACHE_KEY);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
150
|
+
console.warn('[QueryClient] Failed to remove cache', error);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
109
153
|
};
|
|
110
154
|
exports.clearQueryCache = clearQueryCache;
|