@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.
Files changed (58) hide show
  1. package/lib/commonjs/ui/components/FollowButton.js +23 -3
  2. package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
  3. package/lib/commonjs/ui/components/OxyProvider.js +2 -0
  4. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  5. package/lib/commonjs/ui/context/OxyContext.js +49 -9
  6. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  7. package/lib/commonjs/ui/hooks/queries/queryKeys.js +5 -0
  8. package/lib/commonjs/ui/hooks/queries/queryKeys.js.map +1 -1
  9. package/lib/commonjs/ui/hooks/useFollow.js +35 -12
  10. package/lib/commonjs/ui/hooks/useFollow.js.map +1 -1
  11. package/lib/commonjs/ui/stores/followStore.js +68 -0
  12. package/lib/commonjs/ui/stores/followStore.js.map +1 -1
  13. package/lib/module/ui/components/FollowButton.js +23 -3
  14. package/lib/module/ui/components/FollowButton.js.map +1 -1
  15. package/lib/module/ui/components/OxyProvider.js +2 -0
  16. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  17. package/lib/module/ui/context/OxyContext.js +49 -9
  18. package/lib/module/ui/context/OxyContext.js.map +1 -1
  19. package/lib/module/ui/hooks/queries/queryKeys.js +5 -0
  20. package/lib/module/ui/hooks/queries/queryKeys.js.map +1 -1
  21. package/lib/module/ui/hooks/useFollow.js +36 -13
  22. package/lib/module/ui/hooks/useFollow.js.map +1 -1
  23. package/lib/module/ui/stores/followStore.js +68 -0
  24. package/lib/module/ui/stores/followStore.js.map +1 -1
  25. package/lib/typescript/commonjs/ui/components/FollowButton.d.ts +2 -1
  26. package/lib/typescript/commonjs/ui/components/FollowButton.d.ts.map +1 -1
  27. package/lib/typescript/commonjs/ui/components/OxyProvider.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +10 -0
  29. package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
  30. package/lib/typescript/commonjs/ui/hooks/queries/queryKeys.d.ts +4 -0
  31. package/lib/typescript/commonjs/ui/hooks/queries/queryKeys.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts +3 -1
  33. package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/ui/stores/followStore.d.ts +2 -1
  35. package/lib/typescript/commonjs/ui/stores/followStore.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/ui/types/navigation.d.ts +17 -0
  37. package/lib/typescript/commonjs/ui/types/navigation.d.ts.map +1 -1
  38. package/lib/typescript/module/ui/components/FollowButton.d.ts +2 -1
  39. package/lib/typescript/module/ui/components/FollowButton.d.ts.map +1 -1
  40. package/lib/typescript/module/ui/components/OxyProvider.d.ts.map +1 -1
  41. package/lib/typescript/module/ui/context/OxyContext.d.ts +10 -0
  42. package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
  43. package/lib/typescript/module/ui/hooks/queries/queryKeys.d.ts +4 -0
  44. package/lib/typescript/module/ui/hooks/queries/queryKeys.d.ts.map +1 -1
  45. package/lib/typescript/module/ui/hooks/useFollow.d.ts +3 -1
  46. package/lib/typescript/module/ui/hooks/useFollow.d.ts.map +1 -1
  47. package/lib/typescript/module/ui/stores/followStore.d.ts +2 -1
  48. package/lib/typescript/module/ui/stores/followStore.d.ts.map +1 -1
  49. package/lib/typescript/module/ui/types/navigation.d.ts +17 -0
  50. package/lib/typescript/module/ui/types/navigation.d.ts.map +1 -1
  51. package/package.json +3 -3
  52. package/src/ui/components/FollowButton.tsx +26 -4
  53. package/src/ui/components/OxyProvider.tsx +2 -0
  54. package/src/ui/context/OxyContext.tsx +59 -8
  55. package/src/ui/hooks/queries/queryKeys.ts +6 -0
  56. package/src/ui/hooks/useFollow.ts +37 -12
  57. package/src/ui/stores/followStore.ts +47 -1
  58. package/src/ui/types/navigation.ts +17 -0
@@ -1,8 +1,10 @@
1
- import { useCallback, useMemo, useEffect } from 'react';
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 { logger as loggerUtil, type OxyServices, type BulkFollowResult } from '@oxyhq/core';
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 when hook is used for a single user and counts are missing.
132
- useEffect(() => {
133
- if (!isSingleUser || !userId) return;
134
-
135
- if ((followerCount === null || followingCount === null) && !isLoadingCounts) {
136
- fetchUserCounts().catch((error: unknown) => {
137
- loggerUtil.warn('useFollow: fetchUserCounts failed', { component: 'useFollow' }, error);
138
- });
139
- }
140
- }, [isSingleUser, userId, followerCount, followingCount, isLoadingCounts, fetchUserCounts]);
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
  }