@oxyhq/services 10.3.2 → 10.4.0
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/lib/commonjs/ui/components/FollowButton.js +23 -3
- package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
- package/lib/commonjs/ui/components/OxyProvider.js +2 -0
- package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
- package/lib/commonjs/ui/context/OxyContext.js +49 -9
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/hooks/queries/queryKeys.js +5 -0
- package/lib/commonjs/ui/hooks/queries/queryKeys.js.map +1 -1
- package/lib/commonjs/ui/hooks/useFollow.js +35 -12
- package/lib/commonjs/ui/hooks/useFollow.js.map +1 -1
- package/lib/commonjs/ui/stores/followStore.js +68 -0
- package/lib/commonjs/ui/stores/followStore.js.map +1 -1
- package/lib/module/ui/components/FollowButton.js +23 -3
- package/lib/module/ui/components/FollowButton.js.map +1 -1
- package/lib/module/ui/components/OxyProvider.js +2 -0
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/context/OxyContext.js +49 -9
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/hooks/queries/queryKeys.js +5 -0
- package/lib/module/ui/hooks/queries/queryKeys.js.map +1 -1
- package/lib/module/ui/hooks/useFollow.js +36 -13
- package/lib/module/ui/hooks/useFollow.js.map +1 -1
- package/lib/module/ui/stores/followStore.js +68 -0
- package/lib/module/ui/stores/followStore.js.map +1 -1
- package/lib/typescript/commonjs/ui/components/FollowButton.d.ts +2 -1
- package/lib/typescript/commonjs/ui/components/FollowButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +10 -0
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/queries/queryKeys.d.ts +4 -0
- package/lib/typescript/commonjs/ui/hooks/queries/queryKeys.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts +3 -1
- package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/stores/followStore.d.ts +2 -1
- package/lib/typescript/commonjs/ui/stores/followStore.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/types/navigation.d.ts +17 -0
- package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/FollowButton.d.ts +2 -1
- package/lib/typescript/module/ui/components/FollowButton.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/OxyContext.d.ts +10 -0
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/queries/queryKeys.d.ts +4 -0
- package/lib/typescript/module/ui/hooks/queries/queryKeys.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useFollow.d.ts +3 -1
- package/lib/typescript/module/ui/hooks/useFollow.d.ts.map +1 -1
- package/lib/typescript/module/ui/stores/followStore.d.ts +2 -1
- package/lib/typescript/module/ui/stores/followStore.d.ts.map +1 -1
- package/lib/typescript/module/ui/types/navigation.d.ts +17 -0
- package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/ui/components/FollowButton.tsx +26 -4
- package/src/ui/components/OxyProvider.tsx +2 -0
- package/src/ui/context/OxyContext.tsx +59 -8
- package/src/ui/hooks/queries/queryKeys.ts +6 -0
- package/src/ui/hooks/useFollow.ts +37 -12
- package/src/ui/stores/followStore.ts +47 -1
- package/src/ui/types/navigation.ts +17 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { useCallback, useMemo
|
|
1
|
+
import { useCallback, useMemo } from 'react';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
3
|
import { useFollowStore } from '../stores/followStore';
|
|
3
4
|
import { useOxy } from '../context/OxyContext';
|
|
4
|
-
import {
|
|
5
|
+
import { type OxyServices, type BulkFollowResult, type BulkUnfollowResult } from '@oxyhq/core';
|
|
5
6
|
import { useShallow } from 'zustand/react/shallow';
|
|
7
|
+
import { queryKeys } from './queries/queryKeys';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* useFollow — Hook for follow state management.
|
|
@@ -21,6 +23,9 @@ export const useFollow = (userId?: string | string[]) => {
|
|
|
21
23
|
const { oxyServices, canUsePrivateApi } = useOxy();
|
|
22
24
|
const userIds = useMemo(() => (Array.isArray(userId) ? userId : userId ? [userId] : []), [userId]);
|
|
23
25
|
const isSingleUser = typeof userId === 'string';
|
|
26
|
+
// Narrowed single-user id for use in closures (callbacks/queryFn) where TS
|
|
27
|
+
// can't carry the `isSingleUser` boolean back to a `string` narrowing.
|
|
28
|
+
const singleUserId: string | undefined = typeof userId === 'string' ? userId : undefined;
|
|
24
29
|
|
|
25
30
|
// Granular Zustand selectors — only re-render when THIS user's data changes
|
|
26
31
|
const isFollowing = useFollowStore(
|
|
@@ -128,16 +133,29 @@ export const useFollow = (userId?: string | string[]) => {
|
|
|
128
133
|
useFollowStore.getState().setFollowingCount(userId, count);
|
|
129
134
|
}, [isSingleUser, userId]);
|
|
130
135
|
|
|
131
|
-
// Auto-fetch counts
|
|
132
|
-
useEffect
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
136
|
+
// Auto-fetch counts for single-user mode via React Query instead of a manual
|
|
137
|
+
// useEffect. The Zustand store remains the canonical home for count values
|
|
138
|
+
// (it is also written by toggleFollowUser / updateCountsFromFollowAction), so
|
|
139
|
+
// components keep reading `followerCount` / `followingCount` through the
|
|
140
|
+
// granular selectors above — this query only owns the FETCH lifecycle
|
|
141
|
+
// (dedup, caching, retry/backoff). It stays disabled until the counts are
|
|
142
|
+
// actually missing, reproducing the old effect's `(followerCount === null ||
|
|
143
|
+
// followingCount === null)` gate without a render-phase side effect. The
|
|
144
|
+
// store action does the network call and writes the counts; the query simply
|
|
145
|
+
// surfaces the resolved pair as its cached data.
|
|
146
|
+
useQuery({
|
|
147
|
+
queryKey: singleUserId ? queryKeys.follow.counts(singleUserId) : queryKeys.follow.all,
|
|
148
|
+
queryFn: async () => {
|
|
149
|
+
if (!singleUserId) return null;
|
|
150
|
+
await useFollowStore.getState().fetchUserCounts(singleUserId, oxyServices);
|
|
151
|
+
const state = useFollowStore.getState();
|
|
152
|
+
return {
|
|
153
|
+
followers: state.followerCounts[singleUserId] ?? null,
|
|
154
|
+
following: state.followingCounts[singleUserId] ?? null,
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
enabled: !!singleUserId && followerCount === null && followingCount === null,
|
|
158
|
+
});
|
|
141
159
|
|
|
142
160
|
// Multi-user callbacks
|
|
143
161
|
const toggleFollowForUser = useCallback(async (targetUserId: string) => {
|
|
@@ -167,6 +185,12 @@ export const useFollow = (userId?: string | string[]) => {
|
|
|
167
185
|
return useFollowStore.getState().followManyUsers(userIds, oxyServices);
|
|
168
186
|
}, [canUsePrivateApi, userIds, oxyServices]);
|
|
169
187
|
|
|
188
|
+
// Bulk unfollow — unfollows ALL users in ONE network call (idempotent; never follows).
|
|
189
|
+
const unfollowAllUsers = useCallback(async (): Promise<BulkUnfollowResult> => {
|
|
190
|
+
if (!canUsePrivateApi) throw new Error('Authentication is required to unfollow users');
|
|
191
|
+
return useFollowStore.getState().unfollowManyUsers(userIds, oxyServices);
|
|
192
|
+
}, [canUsePrivateApi, userIds, oxyServices]);
|
|
193
|
+
|
|
170
194
|
const clearErrorForUser = useCallback((targetUserId: string) => {
|
|
171
195
|
useFollowStore.getState().clearFollowError(targetUserId);
|
|
172
196
|
}, []);
|
|
@@ -201,6 +225,7 @@ export const useFollow = (userId?: string | string[]) => {
|
|
|
201
225
|
fetchStatusForUser,
|
|
202
226
|
fetchAllStatuses,
|
|
203
227
|
followAllUsers,
|
|
228
|
+
unfollowAllUsers,
|
|
204
229
|
clearErrorForUser,
|
|
205
230
|
isAnyLoading: multiUserLoadingState.isAnyLoading,
|
|
206
231
|
hasAnyError: multiUserLoadingState.hasAnyError,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { create } from 'zustand';
|
|
2
|
-
import type { OxyServices, BulkFollowResult } from '@oxyhq/core';
|
|
2
|
+
import type { OxyServices, BulkFollowResult, BulkUnfollowResult } from '@oxyhq/core';
|
|
3
3
|
|
|
4
4
|
interface FollowState {
|
|
5
5
|
followingUsers: Record<string, boolean>;
|
|
@@ -18,6 +18,8 @@ interface FollowState {
|
|
|
18
18
|
toggleFollowUser: (userId: string, oxyServices: OxyServices, isCurrentlyFollowing: boolean) => Promise<void>;
|
|
19
19
|
// Bulk follow — follows MANY users in one network call; never unfollows.
|
|
20
20
|
followManyUsers: (userIds: string[], oxyServices: OxyServices) => Promise<BulkFollowResult>;
|
|
21
|
+
// Bulk unfollow — unfollows MANY users in one network call; idempotent, never follows.
|
|
22
|
+
unfollowManyUsers: (userIds: string[], oxyServices: OxyServices) => Promise<BulkUnfollowResult>;
|
|
21
23
|
// New methods for follower counts
|
|
22
24
|
setFollowerCount: (userId: string, count: number) => void;
|
|
23
25
|
setFollowingCount: (userId: string, count: number) => void;
|
|
@@ -172,6 +174,50 @@ export const useFollowStore = create<FollowState>((set: any, get: any) => ({
|
|
|
172
174
|
throw error;
|
|
173
175
|
}
|
|
174
176
|
},
|
|
177
|
+
unfollowManyUsers: async (userIds: string[], oxyServices: OxyServices): Promise<BulkUnfollowResult> => {
|
|
178
|
+
set((state: FollowState) => {
|
|
179
|
+
const loadingUsers = { ...state.loadingUsers };
|
|
180
|
+
const errors = { ...state.errors };
|
|
181
|
+
for (const uid of userIds) {
|
|
182
|
+
loadingUsers[uid] = true;
|
|
183
|
+
errors[uid] = null;
|
|
184
|
+
}
|
|
185
|
+
return { loadingUsers, errors };
|
|
186
|
+
});
|
|
187
|
+
try {
|
|
188
|
+
const result = await oxyServices.unfollowUsers(userIds);
|
|
189
|
+
set((state: FollowState) => {
|
|
190
|
+
const followingUsers = { ...state.followingUsers };
|
|
191
|
+
const loadingUsers = { ...state.loadingUsers };
|
|
192
|
+
const errors = { ...state.errors };
|
|
193
|
+
for (const uid of userIds) {
|
|
194
|
+
loadingUsers[uid] = false;
|
|
195
|
+
}
|
|
196
|
+
for (const entry of result.results) {
|
|
197
|
+
if (entry.success) {
|
|
198
|
+
followingUsers[entry.userId] = false;
|
|
199
|
+
errors[entry.userId] = null;
|
|
200
|
+
} else {
|
|
201
|
+
errors[entry.userId] = 'Failed to update follow status';
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return { followingUsers, loadingUsers, errors };
|
|
205
|
+
});
|
|
206
|
+
return result;
|
|
207
|
+
} catch (error: unknown) {
|
|
208
|
+
const message = (error instanceof Error ? error.message : null) || 'Failed to update follow status';
|
|
209
|
+
set((state: FollowState) => {
|
|
210
|
+
const loadingUsers = { ...state.loadingUsers };
|
|
211
|
+
const errors = { ...state.errors };
|
|
212
|
+
for (const uid of userIds) {
|
|
213
|
+
loadingUsers[uid] = false;
|
|
214
|
+
errors[uid] = message;
|
|
215
|
+
}
|
|
216
|
+
return { loadingUsers, errors };
|
|
217
|
+
});
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
},
|
|
175
221
|
setFollowerCount: (userId: string, count: number) => set((state: FollowState) => ({
|
|
176
222
|
followerCounts: { ...state.followerCounts, [userId]: count },
|
|
177
223
|
})),
|
|
@@ -63,4 +63,21 @@ export interface OxyProviderProps {
|
|
|
63
63
|
authWebUrl?: string;
|
|
64
64
|
authRedirectUri?: string;
|
|
65
65
|
queryClient?: QueryClient;
|
|
66
|
+
/**
|
|
67
|
+
* When `true`, skips ONLY the terminal SSO bounce in the web cold-boot
|
|
68
|
+
* chain — the force-redirect to `auth.<apex>/sso?prompt=none` that fires
|
|
69
|
+
* for a visitor with no recoverable local session. This lets a truly
|
|
70
|
+
* anonymous user keep browsing instead of being bounced to the central
|
|
71
|
+
* IdP (e.g. a marketplace that allows anonymous browsing like eBay /
|
|
72
|
+
* Shop.app).
|
|
73
|
+
*
|
|
74
|
+
* Session restore still runs in full: the callback consume, FedCM silent,
|
|
75
|
+
* first-party `/auth/silent` iframe, stored-session bearer, and
|
|
76
|
+
* cookie-restore steps all execute — so a returning signed-in user is
|
|
77
|
+
* still silently restored. Only the force-bounce for a genuinely
|
|
78
|
+
* anonymous visitor is suppressed.
|
|
79
|
+
*
|
|
80
|
+
* Default `false` (current behavior: the bounce fires).
|
|
81
|
+
*/
|
|
82
|
+
disableAutoSso?: boolean;
|
|
66
83
|
}
|