@oxyhq/services 10.2.1 → 10.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -11
- package/lib/commonjs/ui/components/FollowButton.js +3 -1
- package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +24 -13
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAuth.js +1 -1
- package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
- package/lib/commonjs/ui/hooks/useFollow.js +21 -7
- package/lib/commonjs/ui/hooks/useFollow.js.map +1 -1
- package/lib/module/ui/components/FollowButton.js +3 -1
- package/lib/module/ui/components/FollowButton.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +25 -14
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/useAuth.js +1 -1
- package/lib/module/ui/hooks/useAuth.js.map +1 -1
- package/lib/module/ui/hooks/useFollow.js +21 -7
- package/lib/module/ui/hooks/useFollow.js.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts +1 -1
- package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useFollow.d.ts +1 -1
- package/lib/typescript/module/ui/hooks/useFollow.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/ui/components/FollowButton.tsx +2 -2
- package/src/ui/context/OxyContext.tsx +28 -13
- package/src/ui/hooks/useAuth.ts +1 -1
- package/src/ui/hooks/useFollow.ts +16 -8
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
useCallback,
|
|
5
5
|
useContext,
|
|
6
6
|
useEffect,
|
|
7
|
+
useLayoutEffect,
|
|
7
8
|
useMemo,
|
|
8
9
|
useRef,
|
|
9
10
|
useState,
|
|
@@ -287,6 +288,12 @@ function isSameSiteIdP(idpOrigin: string): boolean {
|
|
|
287
288
|
return idpHostname === pageApex || idpHostname.endsWith(`.${pageApex}`);
|
|
288
289
|
}
|
|
289
290
|
|
|
291
|
+
function isOnSsoCallbackPath(): boolean {
|
|
292
|
+
return isWebBrowser() && window.location.pathname === SSO_CALLBACK_PATH;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const useBrowserLayoutEffect = typeof document !== 'undefined' ? useLayoutEffect : useEffect;
|
|
296
|
+
|
|
290
297
|
let cachedUseFollowHook: UseFollowHook | null = null;
|
|
291
298
|
|
|
292
299
|
const loadUseFollowHook = (): UseFollowHook => {
|
|
@@ -387,6 +394,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
387
394
|
const [authResolved, setAuthResolved] = useState(false);
|
|
388
395
|
const authResolvedRef = useRef(false);
|
|
389
396
|
const [initialized, setInitialized] = useState(false);
|
|
397
|
+
const [ssoCallbackIntercepting, setSsoCallbackIntercepting] = useState(false);
|
|
390
398
|
const setAuthState = useAuthStore.setState;
|
|
391
399
|
|
|
392
400
|
// Keep the shared `oxyClient` singleton's token store in lockstep with the
|
|
@@ -1293,13 +1301,12 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1293
1301
|
// +not-found screen before the storage-gated cold-boot `sso-return` step gets
|
|
1294
1302
|
// a chance to strip the fragment and restore the real destination.
|
|
1295
1303
|
//
|
|
1296
|
-
// This effect fires the SAME `runSsoReturn` kernel the instant we
|
|
1297
|
-
// callback path, BEFORE the cold boot (which awaits storage init).
|
|
1298
|
-
//
|
|
1299
|
-
//
|
|
1300
|
-
//
|
|
1301
|
-
//
|
|
1302
|
-
// across every consumer with zero per-app code.
|
|
1304
|
+
// This effect fires the SAME `runSsoReturn` kernel the instant we hydrate ON
|
|
1305
|
+
// the callback path, BEFORE the cold boot (which awaits storage init). The
|
|
1306
|
+
// first render intentionally matches the app/router's static HTML; the
|
|
1307
|
+
// browser layout effect then hides the internal route and consumes the
|
|
1308
|
+
// callback before the first visible paint. That keeps SSR/SSG hydration stable
|
|
1309
|
+
// while still ensuring no app needs a `/__oxy/sso-callback` route.
|
|
1303
1310
|
//
|
|
1304
1311
|
// It is purely ADDITIVE. The later cold-boot `sso-return` step stays as
|
|
1305
1312
|
// defense-in-depth for the non-callback-path case; `consumeSsoReturn` strips
|
|
@@ -1315,13 +1322,13 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1315
1322
|
// already wired when this fires at eager-mount time. If for any reason it were
|
|
1316
1323
|
// not yet set, the later cold-boot `sso-return` step would commit it — but the
|
|
1317
1324
|
// ref IS set during render, so the eager `ok` commit works.
|
|
1318
|
-
|
|
1319
|
-
if (!
|
|
1320
|
-
|
|
1321
|
-
}
|
|
1322
|
-
if (window.location.pathname !== SSO_CALLBACK_PATH) {
|
|
1325
|
+
useBrowserLayoutEffect(() => {
|
|
1326
|
+
if (!isOnSsoCallbackPath()) {
|
|
1327
|
+
setSsoCallbackIntercepting(false);
|
|
1323
1328
|
return;
|
|
1324
1329
|
}
|
|
1330
|
+
let mounted = true;
|
|
1331
|
+
setSsoCallbackIntercepting(true);
|
|
1325
1332
|
runSsoReturnRef.current().catch((error) => {
|
|
1326
1333
|
if (__DEV__) {
|
|
1327
1334
|
loggerUtil.debug(
|
|
@@ -1330,7 +1337,15 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1330
1337
|
error,
|
|
1331
1338
|
);
|
|
1332
1339
|
}
|
|
1340
|
+
}).finally(() => {
|
|
1341
|
+
if (mounted) {
|
|
1342
|
+
setSsoCallbackIntercepting(false);
|
|
1343
|
+
}
|
|
1333
1344
|
});
|
|
1345
|
+
|
|
1346
|
+
return () => {
|
|
1347
|
+
mounted = false;
|
|
1348
|
+
};
|
|
1334
1349
|
}, []);
|
|
1335
1350
|
|
|
1336
1351
|
// Web SSO: automatically check for cross-domain session on web platforms.
|
|
@@ -1718,7 +1733,7 @@ export const OxyProvider: React.FC<OxyContextProviderProps> = ({
|
|
|
1718
1733
|
|
|
1719
1734
|
return (
|
|
1720
1735
|
<OxyContext.Provider value={contextValue}>
|
|
1721
|
-
{children}
|
|
1736
|
+
{ssoCallbackIntercepting ? null : children}
|
|
1722
1737
|
</OxyContext.Provider>
|
|
1723
1738
|
);
|
|
1724
1739
|
};
|
package/src/ui/hooks/useAuth.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useMemo, useEffect } from 'react';
|
|
2
2
|
import { useFollowStore } from '../stores/followStore';
|
|
3
3
|
import { useOxy } from '../context/OxyContext';
|
|
4
|
-
import type
|
|
4
|
+
import { logger as loggerUtil, type OxyServices } from '@oxyhq/core';
|
|
5
5
|
import { useShallow } from 'zustand/react/shallow';
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -18,7 +18,8 @@ import { useShallow } from 'zustand/react/shallow';
|
|
|
18
18
|
* them in selectors would cause unnecessary selector recalculations).
|
|
19
19
|
*/
|
|
20
20
|
export const useFollow = (userId?: string | string[]) => {
|
|
21
|
-
const { oxyServices } = useOxy();
|
|
21
|
+
const { oxyServices, isAuthenticated, isAuthResolved, isTokenReady } = useOxy();
|
|
22
|
+
const canUsePrivateApi = isAuthResolved && isTokenReady && isAuthenticated;
|
|
22
23
|
const userIds = useMemo(() => (Array.isArray(userId) ? userId : userId ? [userId] : []), [userId]);
|
|
23
24
|
const isSingleUser = typeof userId === 'string';
|
|
24
25
|
|
|
@@ -75,9 +76,10 @@ export const useFollow = (userId?: string | string[]) => {
|
|
|
75
76
|
// Store actions are accessed via getState() to avoid subscribing to them.
|
|
76
77
|
const toggleFollow = useCallback(async () => {
|
|
77
78
|
if (!isSingleUser || !userId) throw new Error('toggleFollow is only available for single user mode');
|
|
79
|
+
if (!canUsePrivateApi) throw new Error('Authentication is required to follow users');
|
|
78
80
|
const currentlyFollowing = useFollowStore.getState().followingUsers[userId] ?? false;
|
|
79
81
|
await useFollowStore.getState().toggleFollowUser(userId, oxyServices, currentlyFollowing);
|
|
80
|
-
}, [isSingleUser, userId, oxyServices]);
|
|
82
|
+
}, [isSingleUser, userId, canUsePrivateApi, oxyServices]);
|
|
81
83
|
|
|
82
84
|
const setFollowStatus = useCallback((following: boolean) => {
|
|
83
85
|
if (!isSingleUser || !userId) throw new Error('setFollowStatus is only available for single user mode');
|
|
@@ -86,8 +88,9 @@ export const useFollow = (userId?: string | string[]) => {
|
|
|
86
88
|
|
|
87
89
|
const fetchStatus = useCallback(async () => {
|
|
88
90
|
if (!isSingleUser || !userId) throw new Error('fetchStatus is only available for single user mode');
|
|
91
|
+
if (!canUsePrivateApi) return;
|
|
89
92
|
await useFollowStore.getState().fetchFollowStatus(userId, oxyServices);
|
|
90
|
-
}, [isSingleUser, userId, oxyServices]);
|
|
93
|
+
}, [isSingleUser, userId, canUsePrivateApi, oxyServices]);
|
|
91
94
|
|
|
92
95
|
const clearError = useCallback(() => {
|
|
93
96
|
if (!isSingleUser || !userId) throw new Error('clearError is only available for single user mode');
|
|
@@ -114,28 +117,33 @@ export const useFollow = (userId?: string | string[]) => {
|
|
|
114
117
|
if (!isSingleUser || !userId) return;
|
|
115
118
|
|
|
116
119
|
if ((followerCount === null || followingCount === null) && !isLoadingCounts) {
|
|
117
|
-
fetchUserCounts().catch((
|
|
120
|
+
fetchUserCounts().catch((error: unknown) => {
|
|
121
|
+
loggerUtil.warn('useFollow: fetchUserCounts failed', { component: 'useFollow' }, error);
|
|
122
|
+
});
|
|
118
123
|
}
|
|
119
124
|
}, [isSingleUser, userId, followerCount, followingCount, isLoadingCounts, fetchUserCounts]);
|
|
120
125
|
|
|
121
126
|
// Multi-user callbacks
|
|
122
127
|
const toggleFollowForUser = useCallback(async (targetUserId: string) => {
|
|
128
|
+
if (!canUsePrivateApi) throw new Error('Authentication is required to follow users');
|
|
123
129
|
const currentState = useFollowStore.getState().followingUsers[targetUserId] ?? false;
|
|
124
130
|
await useFollowStore.getState().toggleFollowUser(targetUserId, oxyServices, currentState);
|
|
125
|
-
}, [oxyServices]);
|
|
131
|
+
}, [canUsePrivateApi, oxyServices]);
|
|
126
132
|
|
|
127
133
|
const setFollowStatusForUser = useCallback((targetUserId: string, following: boolean) => {
|
|
128
134
|
useFollowStore.getState().setFollowingStatus(targetUserId, following);
|
|
129
135
|
}, []);
|
|
130
136
|
|
|
131
137
|
const fetchStatusForUser = useCallback(async (targetUserId: string) => {
|
|
138
|
+
if (!canUsePrivateApi) return;
|
|
132
139
|
await useFollowStore.getState().fetchFollowStatus(targetUserId, oxyServices);
|
|
133
|
-
}, [oxyServices]);
|
|
140
|
+
}, [canUsePrivateApi, oxyServices]);
|
|
134
141
|
|
|
135
142
|
const fetchAllStatuses = useCallback(async () => {
|
|
143
|
+
if (!canUsePrivateApi) return;
|
|
136
144
|
const store = useFollowStore.getState();
|
|
137
145
|
await Promise.all(userIds.map(uid => store.fetchFollowStatus(uid, oxyServices)));
|
|
138
|
-
}, [userIds, oxyServices]);
|
|
146
|
+
}, [canUsePrivateApi, userIds, oxyServices]);
|
|
139
147
|
|
|
140
148
|
const clearErrorForUser = useCallback((targetUserId: string) => {
|
|
141
149
|
useFollowStore.getState().clearFollowError(targetUserId);
|