@oxyhq/services 6.9.11 → 6.9.13

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 (117) hide show
  1. package/lib/commonjs/ui/components/FollowButton.js +135 -118
  2. package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
  3. package/lib/commonjs/ui/components/GroupedSection.js.map +1 -1
  4. package/lib/commonjs/ui/components/fileManagement/FileDetailsModal.js.map +1 -1
  5. package/lib/commonjs/ui/components/fileManagement/UploadPreview.js.map +1 -1
  6. package/lib/commonjs/ui/components/internal/GroupedPillButtons.js.map +1 -1
  7. package/lib/commonjs/ui/components/payment/PaymentReviewStep.js.map +1 -1
  8. package/lib/commonjs/ui/components/payment/PaymentSummaryStep.js.map +1 -1
  9. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +6 -2
  10. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  11. package/lib/commonjs/ui/hooks/mutations/useServicesMutations.js.map +1 -1
  12. package/lib/commonjs/ui/hooks/useAuth.js +9 -3
  13. package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
  14. package/lib/commonjs/ui/hooks/useFollow.js +134 -74
  15. package/lib/commonjs/ui/hooks/useFollow.js.map +1 -1
  16. package/lib/commonjs/ui/hooks/useWebSSO.js.map +1 -1
  17. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js +30 -11
  18. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js.map +1 -1
  19. package/lib/commonjs/ui/screens/FAQScreen.js +1 -0
  20. package/lib/commonjs/ui/screens/FAQScreen.js.map +1 -1
  21. package/lib/commonjs/ui/screens/FeedbackScreen.js.map +1 -1
  22. package/lib/commonjs/ui/screens/FileManagementScreen.js +0 -1
  23. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  24. package/lib/commonjs/ui/screens/HistoryViewScreen.js.map +1 -1
  25. package/lib/commonjs/ui/screens/PremiumSubscriptionScreen.js.map +1 -1
  26. package/lib/commonjs/ui/screens/karma/KarmaRewardsScreen.js.map +1 -1
  27. package/lib/commonjs/ui/stores/fileStore.js +6 -6
  28. package/lib/commonjs/ui/stores/fileStore.js.map +1 -1
  29. package/lib/commonjs/ui/utils/fileManagement.js +6 -3
  30. package/lib/commonjs/ui/utils/fileManagement.js.map +1 -1
  31. package/lib/module/ui/components/FollowButton.js +137 -121
  32. package/lib/module/ui/components/FollowButton.js.map +1 -1
  33. package/lib/module/ui/components/GroupedSection.js.map +1 -1
  34. package/lib/module/ui/components/fileManagement/FileDetailsModal.js.map +1 -1
  35. package/lib/module/ui/components/fileManagement/UploadPreview.js.map +1 -1
  36. package/lib/module/ui/components/internal/GroupedPillButtons.js.map +1 -1
  37. package/lib/module/ui/components/payment/PaymentReviewStep.js.map +1 -1
  38. package/lib/module/ui/components/payment/PaymentSummaryStep.js.map +1 -1
  39. package/lib/module/ui/context/hooks/useAuthOperations.js +7 -2
  40. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  41. package/lib/module/ui/hooks/mutations/useServicesMutations.js.map +1 -1
  42. package/lib/module/ui/hooks/useAuth.js +9 -3
  43. package/lib/module/ui/hooks/useAuth.js.map +1 -1
  44. package/lib/module/ui/hooks/useFollow.js +132 -72
  45. package/lib/module/ui/hooks/useFollow.js.map +1 -1
  46. package/lib/module/ui/hooks/useWebSSO.js.map +1 -1
  47. package/lib/module/ui/screens/EditProfileFieldScreen.js +30 -11
  48. package/lib/module/ui/screens/EditProfileFieldScreen.js.map +1 -1
  49. package/lib/module/ui/screens/FAQScreen.js +1 -0
  50. package/lib/module/ui/screens/FAQScreen.js.map +1 -1
  51. package/lib/module/ui/screens/FeedbackScreen.js.map +1 -1
  52. package/lib/module/ui/screens/FileManagementScreen.js +0 -1
  53. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  54. package/lib/module/ui/screens/HistoryViewScreen.js.map +1 -1
  55. package/lib/module/ui/screens/PremiumSubscriptionScreen.js.map +1 -1
  56. package/lib/module/ui/screens/karma/KarmaRewardsScreen.js.map +1 -1
  57. package/lib/module/ui/stores/fileStore.js +6 -6
  58. package/lib/module/ui/stores/fileStore.js.map +1 -1
  59. package/lib/module/ui/utils/fileManagement.js +6 -3
  60. package/lib/module/ui/utils/fileManagement.js.map +1 -1
  61. package/lib/typescript/commonjs/ui/components/FollowButton.d.ts +12 -1
  62. package/lib/typescript/commonjs/ui/components/FollowButton.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/ui/components/GroupedSection.d.ts +5 -0
  64. package/lib/typescript/commonjs/ui/components/GroupedSection.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/ui/components/feedback/types.d.ts +2 -2
  66. package/lib/typescript/commonjs/ui/components/feedback/types.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/ui/components/internal/GroupedPillButtons.d.ts +7 -1
  68. package/lib/typescript/commonjs/ui/components/internal/GroupedPillButtons.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts +31 -0
  72. package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/ui/screens/FAQScreen.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/ui/screens/HistoryViewScreen.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/ui/utils/fileManagement.d.ts.map +1 -1
  78. package/lib/typescript/module/ui/components/FollowButton.d.ts +12 -1
  79. package/lib/typescript/module/ui/components/FollowButton.d.ts.map +1 -1
  80. package/lib/typescript/module/ui/components/GroupedSection.d.ts +5 -0
  81. package/lib/typescript/module/ui/components/GroupedSection.d.ts.map +1 -1
  82. package/lib/typescript/module/ui/components/feedback/types.d.ts +2 -2
  83. package/lib/typescript/module/ui/components/feedback/types.d.ts.map +1 -1
  84. package/lib/typescript/module/ui/components/internal/GroupedPillButtons.d.ts +7 -1
  85. package/lib/typescript/module/ui/components/internal/GroupedPillButtons.d.ts.map +1 -1
  86. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  87. package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
  88. package/lib/typescript/module/ui/hooks/useFollow.d.ts +31 -0
  89. package/lib/typescript/module/ui/hooks/useFollow.d.ts.map +1 -1
  90. package/lib/typescript/module/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  91. package/lib/typescript/module/ui/screens/FAQScreen.d.ts.map +1 -1
  92. package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
  93. package/lib/typescript/module/ui/screens/HistoryViewScreen.d.ts.map +1 -1
  94. package/lib/typescript/module/ui/utils/fileManagement.d.ts.map +1 -1
  95. package/package.json +2 -2
  96. package/src/ui/components/FollowButton.tsx +117 -109
  97. package/src/ui/components/GroupedSection.tsx +5 -0
  98. package/src/ui/components/feedback/types.ts +2 -2
  99. package/src/ui/components/fileManagement/FileDetailsModal.tsx +1 -1
  100. package/src/ui/components/fileManagement/UploadPreview.tsx +1 -1
  101. package/src/ui/components/internal/GroupedPillButtons.tsx +17 -8
  102. package/src/ui/components/payment/PaymentReviewStep.tsx +1 -1
  103. package/src/ui/components/payment/PaymentSummaryStep.tsx +1 -1
  104. package/src/ui/context/hooks/useAuthOperations.ts +8 -3
  105. package/src/ui/hooks/mutations/useServicesMutations.ts +3 -3
  106. package/src/ui/hooks/useAuth.ts +10 -4
  107. package/src/ui/hooks/useFollow.ts +161 -74
  108. package/src/ui/hooks/useWebSSO.ts +3 -3
  109. package/src/ui/screens/EditProfileFieldScreen.tsx +28 -16
  110. package/src/ui/screens/FAQScreen.tsx +2 -1
  111. package/src/ui/screens/FeedbackScreen.tsx +7 -7
  112. package/src/ui/screens/FileManagementScreen.tsx +1 -2
  113. package/src/ui/screens/HistoryViewScreen.tsx +5 -1
  114. package/src/ui/screens/PremiumSubscriptionScreen.tsx +1 -1
  115. package/src/ui/screens/karma/KarmaRewardsScreen.tsx +1 -1
  116. package/src/ui/stores/fileStore.ts +9 -9
  117. package/src/ui/utils/fileManagement.ts +16 -10
@@ -1,114 +1,150 @@
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 { OxyServices } from '@oxyhq/core';
5
+ import { useShallow } from 'zustand/react/shallow';
4
6
 
7
+ /**
8
+ * useFollow — Hook for follow state management.
9
+ *
10
+ * Performance fixes:
11
+ * 1. Uses granular Zustand selectors instead of subscribing to the entire store.
12
+ * The old `useFollowStore()` caused every component using this hook to re-render
13
+ * on ANY store change (any user's follow status, loading state, error, or count).
14
+ * 2. Callbacks depend on primitive values (userId, isFollowing) not object references,
15
+ * so they remain stable across renders.
16
+ * 3. Store action methods are accessed via getState() in callbacks to avoid
17
+ * subscribing to the action functions themselves (they never change but including
18
+ * them in selectors would cause unnecessary selector recalculations).
19
+ */
5
20
  export const useFollow = (userId?: string | string[]) => {
6
21
  const { oxyServices } = useOxy();
7
22
  const userIds = useMemo(() => (Array.isArray(userId) ? userId : userId ? [userId] : []), [userId]);
8
23
  const isSingleUser = typeof userId === 'string';
9
24
 
10
- // Zustand selectors
11
- const followState = useFollowStore();
25
+ // Granular Zustand selectors — only re-render when THIS user's data changes
26
+ const isFollowing = useFollowStore(
27
+ useCallback((s) => (isSingleUser && userId ? s.followingUsers[userId] ?? false : false), [isSingleUser, userId])
28
+ );
29
+ const isLoading = useFollowStore(
30
+ useCallback((s) => (isSingleUser && userId ? s.loadingUsers[userId] ?? false : false), [isSingleUser, userId])
31
+ );
32
+ const error = useFollowStore(
33
+ useCallback((s) => (isSingleUser && userId ? s.errors[userId] ?? null : null), [isSingleUser, userId])
34
+ );
35
+ const followerCount = useFollowStore(
36
+ useCallback((s) => (isSingleUser && userId ? s.followerCounts[userId] ?? null : null), [isSingleUser, userId])
37
+ );
38
+ const followingCount = useFollowStore(
39
+ useCallback((s) => (isSingleUser && userId ? s.followingCounts[userId] ?? null : null), [isSingleUser, userId])
40
+ );
41
+ const isLoadingCounts = useFollowStore(
42
+ useCallback((s) => (isSingleUser && userId ? s.loadingCounts[userId] ?? false : false), [isSingleUser, userId])
43
+ );
12
44
 
13
- // Single user helpers
14
- const isFollowing = isSingleUser && userId ? followState.followingUsers[userId] ?? false : false;
15
- const isLoading = isSingleUser && userId ? followState.loadingUsers[userId] ?? false : false;
16
- const error = isSingleUser && userId ? followState.errors[userId] ?? null : null;
17
-
18
- // Follower count helpers
19
- const followerCount = isSingleUser && userId ? followState.followerCounts[userId] ?? null : null;
20
- const followingCount = isSingleUser && userId ? followState.followingCounts[userId] ?? null : null;
21
- const isLoadingCounts = isSingleUser && userId ? followState.loadingCounts[userId] ?? false : false;
45
+ // For multi-user mode, use shallow comparison to avoid re-renders when unrelated users change
46
+ const followData = useFollowStore(
47
+ useShallow((s) => {
48
+ if (isSingleUser) return {};
49
+ const data: Record<string, { isFollowing: boolean; isLoading: boolean; error: string | null }> = {};
50
+ for (const uid of userIds) {
51
+ data[uid] = {
52
+ isFollowing: s.followingUsers[uid] ?? false,
53
+ isLoading: s.loadingUsers[uid] ?? false,
54
+ error: s.errors[uid] ?? null,
55
+ };
56
+ }
57
+ return data;
58
+ })
59
+ );
22
60
 
61
+ // Multi-user aggregate selectors
62
+ const multiUserLoadingState = useFollowStore(
63
+ useShallow((s) => {
64
+ if (isSingleUser) return { isAnyLoading: false, hasAnyError: false, allFollowing: true, allNotFollowing: true };
65
+ return {
66
+ isAnyLoading: userIds.some(uid => s.loadingUsers[uid]),
67
+ hasAnyError: userIds.some(uid => !!s.errors[uid]),
68
+ allFollowing: userIds.every(uid => s.followingUsers[uid]),
69
+ allNotFollowing: userIds.every(uid => !s.followingUsers[uid]),
70
+ };
71
+ })
72
+ );
73
+
74
+ // Stable callbacks that depend on primitives, not object references.
75
+ // Store actions are accessed via getState() to avoid subscribing to them.
23
76
  const toggleFollow = useCallback(async () => {
24
77
  if (!isSingleUser || !userId) throw new Error('toggleFollow is only available for single user mode');
25
- await followState.toggleFollowUser(userId, oxyServices, isFollowing);
26
- }, [isSingleUser, userId, followState, oxyServices, isFollowing]);
78
+ const currentlyFollowing = useFollowStore.getState().followingUsers[userId] ?? false;
79
+ await useFollowStore.getState().toggleFollowUser(userId, oxyServices, currentlyFollowing);
80
+ }, [isSingleUser, userId, oxyServices]);
27
81
 
28
82
  const setFollowStatus = useCallback((following: boolean) => {
29
83
  if (!isSingleUser || !userId) throw new Error('setFollowStatus is only available for single user mode');
30
- followState.setFollowingStatus(userId, following);
31
- }, [isSingleUser, userId, followState]);
84
+ useFollowStore.getState().setFollowingStatus(userId, following);
85
+ }, [isSingleUser, userId]);
32
86
 
33
87
  const fetchStatus = useCallback(async () => {
34
88
  if (!isSingleUser || !userId) throw new Error('fetchStatus is only available for single user mode');
35
- await followState.fetchFollowStatus(userId, oxyServices);
36
- }, [isSingleUser, userId, followState, oxyServices]);
89
+ await useFollowStore.getState().fetchFollowStatus(userId, oxyServices);
90
+ }, [isSingleUser, userId, oxyServices]);
37
91
 
38
92
  const clearError = useCallback(() => {
39
93
  if (!isSingleUser || !userId) throw new Error('clearError is only available for single user mode');
40
- followState.clearFollowError(userId);
41
- }, [isSingleUser, userId, followState]);
94
+ useFollowStore.getState().clearFollowError(userId);
95
+ }, [isSingleUser, userId]);
42
96
 
43
97
  const fetchUserCounts = useCallback(async () => {
44
98
  if (!isSingleUser || !userId) throw new Error('fetchUserCounts is only available for single user mode');
45
- await followState.fetchUserCounts(userId, oxyServices);
46
- }, [isSingleUser, userId, followState, oxyServices]);
99
+ await useFollowStore.getState().fetchUserCounts(userId, oxyServices);
100
+ }, [isSingleUser, userId, oxyServices]);
47
101
 
48
102
  const setFollowerCount = useCallback((count: number) => {
49
103
  if (!isSingleUser || !userId) throw new Error('setFollowerCount is only available for single user mode');
50
- followState.setFollowerCount(userId, count);
51
- }, [isSingleUser, userId, followState]);
104
+ useFollowStore.getState().setFollowerCount(userId, count);
105
+ }, [isSingleUser, userId]);
52
106
 
53
107
  const setFollowingCount = useCallback((count: number) => {
54
108
  if (!isSingleUser || !userId) throw new Error('setFollowingCount is only available for single user mode');
55
- followState.setFollowingCount(userId, count);
56
- }, [isSingleUser, userId, followState]);
109
+ useFollowStore.getState().setFollowingCount(userId, count);
110
+ }, [isSingleUser, userId]);
57
111
 
58
112
  // Auto-fetch counts when hook is used for a single user and counts are missing.
59
113
  useEffect(() => {
60
114
  if (!isSingleUser || !userId) return;
61
115
 
62
- // If either count is not set and we're not already loading counts, trigger a fetch.
63
116
  if ((followerCount === null || followingCount === null) && !isLoadingCounts) {
64
117
  fetchUserCounts().catch((err: unknown) => console.warn('useFollow: fetchUserCounts failed', err));
65
118
  }
66
119
  }, [isSingleUser, userId, followerCount, followingCount, isLoadingCounts, fetchUserCounts]);
67
120
 
68
- // Multiple user helpers
69
- const followData = useMemo(() => {
70
- const data: Record<string, { isFollowing: boolean; isLoading: boolean; error: string | null }> = {};
71
- userIds.forEach(uid => {
72
- data[uid] = {
73
- isFollowing: followState.followingUsers[uid] ?? false,
74
- isLoading: followState.loadingUsers[uid] ?? false,
75
- error: followState.errors[uid] ?? null,
76
- };
77
- });
78
- return data;
79
- }, [userIds, followState.followingUsers, followState.loadingUsers, followState.errors]);
80
-
121
+ // Multi-user callbacks
81
122
  const toggleFollowForUser = useCallback(async (targetUserId: string) => {
82
- const currentState = followState.followingUsers[targetUserId] ?? false;
83
- await followState.toggleFollowUser(targetUserId, oxyServices, currentState);
84
- }, [followState, oxyServices]);
123
+ const currentState = useFollowStore.getState().followingUsers[targetUserId] ?? false;
124
+ await useFollowStore.getState().toggleFollowUser(targetUserId, oxyServices, currentState);
125
+ }, [oxyServices]);
85
126
 
86
127
  const setFollowStatusForUser = useCallback((targetUserId: string, following: boolean) => {
87
- followState.setFollowingStatus(targetUserId, following);
88
- }, [followState]);
128
+ useFollowStore.getState().setFollowingStatus(targetUserId, following);
129
+ }, []);
89
130
 
90
131
  const fetchStatusForUser = useCallback(async (targetUserId: string) => {
91
- await followState.fetchFollowStatus(targetUserId, oxyServices);
92
- }, [followState, oxyServices]);
132
+ await useFollowStore.getState().fetchFollowStatus(targetUserId, oxyServices);
133
+ }, [oxyServices]);
93
134
 
94
135
  const fetchAllStatuses = useCallback(async () => {
95
- await Promise.all(userIds.map(uid => followState.fetchFollowStatus(uid, oxyServices)));
96
- }, [userIds, followState, oxyServices]);
136
+ const store = useFollowStore.getState();
137
+ await Promise.all(userIds.map(uid => store.fetchFollowStatus(uid, oxyServices)));
138
+ }, [userIds, oxyServices]);
97
139
 
98
140
  const clearErrorForUser = useCallback((targetUserId: string) => {
99
- followState.clearFollowError(targetUserId);
100
- }, [followState]);
141
+ useFollowStore.getState().clearFollowError(targetUserId);
142
+ }, []);
101
143
 
102
144
  const updateCountsFromFollowAction = useCallback((targetUserId: string, action: 'follow' | 'unfollow', counts: { followers: number; following: number }) => {
103
145
  const currentUserId = oxyServices.getCurrentUserId() || undefined;
104
- followState.updateCountsFromFollowAction(targetUserId, action, counts, currentUserId);
105
- }, [followState, oxyServices]);
106
-
107
- // Aggregate helpers for multiple users
108
- const isAnyLoading = userIds.some(uid => followState.loadingUsers[uid]);
109
- const hasAnyError = userIds.some(uid => !!followState.errors[uid]);
110
- const allFollowing = userIds.every(uid => followState.followingUsers[uid]);
111
- const allNotFollowing = userIds.every(uid => !followState.followingUsers[uid]);
146
+ useFollowStore.getState().updateCountsFromFollowAction(targetUserId, action, counts, currentUserId);
147
+ }, [oxyServices]);
112
148
 
113
149
  if (isSingleUser && userId) {
114
150
  return {
@@ -119,7 +155,6 @@ export const useFollow = (userId?: string | string[]) => {
119
155
  setFollowStatus,
120
156
  fetchStatus,
121
157
  clearError,
122
- // Follower count methods
123
158
  followerCount,
124
159
  followingCount,
125
160
  isLoadingCounts,
@@ -136,33 +171,85 @@ export const useFollow = (userId?: string | string[]) => {
136
171
  fetchStatusForUser,
137
172
  fetchAllStatuses,
138
173
  clearErrorForUser,
139
- isAnyLoading,
140
- hasAnyError,
141
- allFollowing,
142
- allNotFollowing,
174
+ isAnyLoading: multiUserLoadingState.isAnyLoading,
175
+ hasAnyError: multiUserLoadingState.hasAnyError,
176
+ allFollowing: multiUserLoadingState.allFollowing,
177
+ allNotFollowing: multiUserLoadingState.allNotFollowing,
178
+ };
179
+ };
180
+
181
+ /**
182
+ * useFollowForButton — Lightweight follow hook for FollowButton in list contexts.
183
+ *
184
+ * Unlike useFollow, this hook:
185
+ * - Accepts oxyServices directly (no useOxy() context subscription)
186
+ * - Only subscribes to the specific user's follow and loading state
187
+ * - Returns only what FollowButton needs (no counts, no multi-user)
188
+ */
189
+ export const useFollowForButton = (userId: string, oxyServices: OxyServices) => {
190
+ const isFollowing = useFollowStore(
191
+ useCallback((s) => s.followingUsers[userId] ?? false, [userId])
192
+ );
193
+ const isLoading = useFollowStore(
194
+ useCallback((s) => s.loadingUsers[userId] ?? false, [userId])
195
+ );
196
+ const error = useFollowStore(
197
+ useCallback((s) => s.errors[userId] ?? null, [userId])
198
+ );
199
+
200
+ const toggleFollow = useCallback(async () => {
201
+ const currentlyFollowing = useFollowStore.getState().followingUsers[userId] ?? false;
202
+ await useFollowStore.getState().toggleFollowUser(userId, oxyServices, currentlyFollowing);
203
+ }, [userId, oxyServices]);
204
+
205
+ const fetchStatus = useCallback(async () => {
206
+ await useFollowStore.getState().fetchFollowStatus(userId, oxyServices);
207
+ }, [userId, oxyServices]);
208
+
209
+ const setFollowStatus = useCallback((following: boolean) => {
210
+ useFollowStore.getState().setFollowingStatus(userId, following);
211
+ }, [userId]);
212
+
213
+ const clearError = useCallback(() => {
214
+ useFollowStore.getState().clearFollowError(userId);
215
+ }, [userId]);
216
+
217
+ return {
218
+ isFollowing,
219
+ isLoading,
220
+ error,
221
+ toggleFollow,
222
+ fetchStatus,
223
+ setFollowStatus,
224
+ clearError,
143
225
  };
144
226
  };
145
227
 
146
228
  // Convenience hook for just follower counts
147
229
  export const useFollowerCounts = (userId: string) => {
148
230
  const { oxyServices } = useOxy();
149
- const followState = useFollowStore();
150
231
 
151
- const followerCount = followState.followerCounts[userId] ?? null;
152
- const followingCount = followState.followingCounts[userId] ?? null;
153
- const isLoadingCounts = followState.loadingCounts[userId] ?? false;
232
+ const followerCount = useFollowStore(
233
+ useCallback((s) => s.followerCounts[userId] ?? null, [userId])
234
+ );
235
+ const followingCount = useFollowStore(
236
+ useCallback((s) => s.followingCounts[userId] ?? null, [userId])
237
+ );
238
+ const isLoadingCounts = useFollowStore(
239
+ useCallback((s) => s.loadingCounts[userId] ?? false, [userId])
240
+ );
154
241
 
155
242
  const fetchUserCounts = useCallback(async () => {
156
- await followState.fetchUserCounts(userId, oxyServices);
157
- }, [userId, followState, oxyServices]);
243
+ await useFollowStore.getState().fetchUserCounts(userId, oxyServices);
244
+ }, [userId, oxyServices]);
158
245
 
159
246
  const setFollowerCount = useCallback((count: number) => {
160
- followState.setFollowerCount(userId, count);
161
- }, [userId, followState]);
247
+ useFollowStore.getState().setFollowerCount(userId, count);
248
+ }, [userId]);
162
249
 
163
250
  const setFollowingCount = useCallback((count: number) => {
164
- followState.setFollowingCount(userId, count);
165
- }, [userId, followState]);
251
+ useFollowStore.getState().setFollowingCount(userId, count);
252
+ }, [userId]);
166
253
 
167
254
  return {
168
255
  followerCount,
@@ -172,4 +259,4 @@ export const useFollowerCounts = (userId: string) => {
172
259
  setFollowerCount,
173
260
  setFollowingCount,
174
261
  };
175
- };
262
+ };
@@ -84,7 +84,7 @@ export function useWebSSO({
84
84
  const hasCheckedRef = useRef(false);
85
85
 
86
86
  // Check FedCM support once
87
- const fedCMSupported = isWebBrowser() && (oxyServices as any).isFedCMSupported?.();
87
+ const fedCMSupported = isWebBrowser() && oxyServices.isFedCMSupported?.();
88
88
 
89
89
  const checkSSO = useCallback(async (): Promise<SessionLoginResponse | null> => {
90
90
  if (!isWebBrowser() || isCheckingRef.current) {
@@ -106,7 +106,7 @@ export function useWebSSO({
106
106
  isCheckingRef.current = true;
107
107
 
108
108
  try {
109
- const session = await (oxyServices as any).silentSignInWithFedCM?.();
109
+ const session = await oxyServices.silentSignInWithFedCM?.();
110
110
 
111
111
  if (session) {
112
112
  await onSessionFound(session);
@@ -142,7 +142,7 @@ export function useWebSSO({
142
142
  isCheckingRef.current = true;
143
143
 
144
144
  try {
145
- const session = await (oxyServices as any).signInWithFedCM?.();
145
+ const session = await oxyServices.signInWithFedCM?.();
146
146
 
147
147
  if (session) {
148
148
  await onSessionFound(session);
@@ -255,31 +255,30 @@ const EditProfileFieldScreen: React.FC<EditProfileFieldScreenProps> = ({
255
255
  useEffect(() => {
256
256
  if (!user) return;
257
257
 
258
- // Cast user to any to access dynamic properties
259
- const userData = user as any;
258
+ const userData = user;
260
259
 
261
260
  if (fieldConfig.isList) {
262
261
  if (fieldType === 'locations') {
263
- const locations = Array.isArray(userData.locations) ? userData.locations : [];
264
- setListItems(locations.map((loc: any, i: number) => ({
265
- id: loc.id || `location-${i}`,
266
- name: loc.name || '',
262
+ const locations = Array.isArray(userData.locations) ? userData.locations as Array<Record<string, unknown>> : [];
263
+ setListItems(locations.map((loc, i) => ({
264
+ id: String(loc.id || `location-${i}`),
265
+ name: String(loc.name || ''),
267
266
  ...loc,
268
267
  })));
269
268
  } else if (fieldType === 'links') {
270
- const linksMetadata = Array.isArray(userData.linksMetadata) ? userData.linksMetadata : [];
269
+ const linksMetadata = Array.isArray(userData.linksMetadata) ? userData.linksMetadata as Array<Record<string, unknown>> : [];
271
270
  const links = Array.isArray(userData.links) ? userData.links : [];
272
271
  // Use linksMetadata if available, otherwise convert links array
273
272
  if (linksMetadata.length > 0) {
274
- setListItems(linksMetadata.map((link: any, i: number) => ({
275
- id: link.id || `link-${i}`,
276
- url: link.url || link.link || '',
277
- title: link.title || '',
273
+ setListItems(linksMetadata.map((link, i) => ({
274
+ id: String(link.id || `link-${i}`),
275
+ url: String(link.url || link.link || ''),
276
+ title: String(link.title || ''),
278
277
  ...link,
279
278
  })));
280
279
  } else {
281
- setListItems(links.map((item: any, i: number) => {
282
- const url = typeof item === 'string' ? item : (item.link || item.url || '');
280
+ setListItems(links.map((item, i) => {
281
+ const url = typeof item === 'string' ? item : (item.link || '');
283
282
  return {
284
283
  id: `link-${i}`,
285
284
  url,
@@ -365,11 +364,24 @@ const EditProfileFieldScreen: React.FC<EditProfileFieldScreenProps> = ({
365
364
  if (fieldConfig.isList) {
366
365
  let success = false;
367
366
  if (fieldType === 'locations') {
368
- success = await saveProfile({ locations: listItems as any });
367
+ success = await saveProfile({
368
+ locations: listItems.map(item => ({
369
+ id: item.id,
370
+ name: String(item.name || ''),
371
+ ...(item.label !== undefined && { label: String(item.label) }),
372
+ ...(item.coordinates !== undefined && { coordinates: item.coordinates as { lat: number; lon: number } }),
373
+ })),
374
+ });
369
375
  } else if (fieldType === 'links') {
370
376
  success = await saveProfile({
371
- linksMetadata: listItems as any,
372
- links: listItems.map((item: any) => item.url),
377
+ linksMetadata: listItems.map(item => ({
378
+ id: item.id,
379
+ url: String(item.url || ''),
380
+ ...(item.title !== undefined && { title: String(item.title) }),
381
+ ...(item.description !== undefined && { description: String(item.description) }),
382
+ ...(item.image !== undefined && { image: String(item.image) }),
383
+ })),
384
+ links: listItems.map(item => String(item.url || '')),
373
385
  });
374
386
  }
375
387
  if (success) {
@@ -263,7 +263,8 @@ const createStyles = (themeStyles: any) => StyleSheet.create({
263
263
  flex: 1,
264
264
  fontSize: 16,
265
265
  ...Platform.select({
266
- web: { outlineStyle: 'none' as any },
266
+ // outlineStyle: 'none' is a valid web CSS property not in React Native's TextStyle definition
267
+ web: { outlineStyle: 'none' as unknown as import('react-native').TextStyle['outlineStyle'] },
267
268
  }),
268
269
  },
269
270
  categoriesContainer: {
@@ -50,7 +50,7 @@ const FeedbackScreen: React.FC<BaseScreenProps> = ({
50
50
  const fadeAnim = useRef(new Animated.Value(1)).current;
51
51
  const slideAnim = useRef(new Animated.Value(0)).current;
52
52
 
53
- const styles = useMemo(() => createFeedbackStyles(colors as any), [colors]);
53
+ const styles = useMemo(() => createFeedbackStyles(colors), [colors]);
54
54
 
55
55
  const animateTransition = useCallback((nextStep: number) => {
56
56
  Animated.timing(fadeAnim, {
@@ -149,7 +149,7 @@ const FeedbackScreen: React.FC<BaseScreenProps> = ({
149
149
  iconColor: type.color,
150
150
  title: type.label,
151
151
  subtitle: type.description,
152
- onPress: () => { updateField('type', type.id as any); updateField('category', ''); },
152
+ onPress: () => { updateField('type', type.id); updateField('category', ''); },
153
153
  selected: feedbackData.type === type.id,
154
154
  showChevron: false,
155
155
  multiRow: true,
@@ -172,7 +172,7 @@ const FeedbackScreen: React.FC<BaseScreenProps> = ({
172
172
  icon: p.icon,
173
173
  iconColor: p.color,
174
174
  title: p.label,
175
- onPress: () => updateField('priority', p.id as any),
175
+ onPress: () => updateField('priority', p.id),
176
176
  selected: feedbackData.priority === p.id,
177
177
  showChevron: false,
178
178
  dense: true,
@@ -246,7 +246,7 @@ const FeedbackScreen: React.FC<BaseScreenProps> = ({
246
246
  onChangeText={(text) => { updateField('title', text); setErrorMessage(''); }}
247
247
  placeholder={t('feedback.fields.title.placeholder') || 'Brief summary of your feedback'}
248
248
  testID="feedback-title-input"
249
- colors={colors as any}
249
+ colors={colors}
250
250
  styles={styles}
251
251
  accessibilityLabel="Feedback title"
252
252
  accessibilityHint="Enter a brief summary of your feedback"
@@ -261,7 +261,7 @@ const FeedbackScreen: React.FC<BaseScreenProps> = ({
261
261
  multiline={true}
262
262
  numberOfLines={6}
263
263
  testID="feedback-description-input"
264
- colors={colors as any}
264
+ colors={colors}
265
265
  styles={styles}
266
266
  accessibilityLabel="Feedback description"
267
267
  accessibilityHint="Provide detailed information about your feedback"
@@ -319,7 +319,7 @@ const FeedbackScreen: React.FC<BaseScreenProps> = ({
319
319
  onChangeText={(text) => { updateField('contactEmail', text); setErrorMessage(''); }}
320
320
  placeholder={user?.email || (t('feedback.fields.email.placeholder') || 'Enter your email address')}
321
321
  testID="feedback-email-input"
322
- colors={colors as any}
322
+ colors={colors}
323
323
  styles={styles}
324
324
  accessibilityLabel="Email address"
325
325
  accessibilityHint="Enter your email so we can respond"
@@ -494,7 +494,7 @@ const FeedbackScreen: React.FC<BaseScreenProps> = ({
494
494
  keyboardShouldPersistTaps="handled"
495
495
  >
496
496
  {feedbackState.status !== 'success' && (
497
- <ProgressIndicator currentStep={currentStep} totalSteps={4} colors={colors as any} styles={styles} />
497
+ <ProgressIndicator currentStep={currentStep} totalSteps={4} colors={colors} styles={styles} />
498
498
  )}
499
499
  {renderCurrentStep()}
500
500
  </ScrollView>
@@ -1506,8 +1506,7 @@ const FileManagementScreen: React.FC<FileManagementScreenProps> = ({
1506
1506
  {file.metadata.description}
1507
1507
  </Text>
1508
1508
  ) : undefined,
1509
- // biome-ignore lint/suspicious/noExplicitAny: GroupedSectionItem has dynamic properties
1510
- } as any;
1509
+ };
1511
1510
  });
1512
1511
  }, [filteredFiles, theme, themeStyles, deleting, handleFileDownload, handleFileDelete, handleFileOpen, getSafeDownloadUrlCallback, selectMode, selectedIds]);
1513
1512
 
@@ -41,7 +41,11 @@ const HistoryViewScreen: React.FC<BaseScreenProps> = ({
41
41
  if (isReactNative) {
42
42
  try {
43
43
  const asyncStorageModule = await import('@react-native-async-storage/async-storage');
44
- const storage = (asyncStorageModule.default as unknown) as any;
44
+ const storage = asyncStorageModule.default as unknown as {
45
+ getItem: (key: string) => Promise<string | null>;
46
+ setItem: (key: string, value: string) => Promise<void>;
47
+ removeItem: (key: string) => Promise<void>;
48
+ };
45
49
  return {
46
50
  getItem: storage.getItem.bind(storage),
47
51
  setItem: storage.setItem.bind(storage),
@@ -915,7 +915,7 @@ const PremiumSubscriptionScreen: React.FC<BaseScreenProps> = ({
915
915
  <View style={styles.featureHeader}>
916
916
  <View style={styles.featureIconContainer}>
917
917
  <Ionicons
918
- name={getCategoryIcon(feature.category) as any}
918
+ name={getCategoryIcon(feature.category) as React.ComponentProps<typeof Ionicons>['name']}
919
919
  size={24}
920
920
  color={getCategoryColor(feature.category)}
921
921
  />
@@ -292,7 +292,7 @@ const KarmaRewardsScreen: React.FC<BaseScreenProps> = ({ goBack, theme }) => {
292
292
  {isLocked ? (
293
293
  <Ionicons name="lock-closed" size={40} color="#8E8E93" />
294
294
  ) : (
295
- <Ionicons name={achievement.icon as any} size={40} color="#FFFFFF" />
295
+ <Ionicons name={achievement.icon as React.ComponentProps<typeof Ionicons>['name']} size={40} color="#FFFFFF" />
296
296
  )}
297
297
  </View>
298
298
 
@@ -5,12 +5,12 @@ import type { FileMetadata } from '@oxyhq/core';
5
5
  function shallowEqualFile(a: FileMetadata, b: FileMetadata): boolean {
6
6
  if (a === b) return true;
7
7
  if (!a || !b) return false;
8
- const aKeys = Object.keys(a) as Array<keyof FileMetadata>;
9
- const bKeys = Object.keys(b) as Array<keyof FileMetadata>;
8
+ const aKeys = Object.keys(a);
9
+ const bKeys = Object.keys(b);
10
10
  if (aKeys.length !== bKeys.length) return false;
11
11
  for (const k of aKeys) {
12
12
  // treat metadata/variants shallowly by reference
13
- if ((a as any)[k] !== (b as any)[k]) return false;
13
+ if (a[k as keyof FileMetadata] !== b[k as keyof FileMetadata]) return false;
14
14
  }
15
15
  return true;
16
16
  }
@@ -60,7 +60,7 @@ export const useFileStore = create<FileState>((set, get) => ({
60
60
  if (sameOrder) {
61
61
  sameFiles = order.every(id => state.files[id] && shallowEqualFile(state.files[id], map[id] as FileMetadata));
62
62
  }
63
- if (sameOrder && sameFiles) return {} as any;
63
+ if (sameOrder && sameFiles) return state;
64
64
  return { files: map, order };
65
65
  }
66
66
  const newFiles = { ...state.files };
@@ -72,13 +72,13 @@ export const useFileStore = create<FileState>((set, get) => ({
72
72
  if (!prev || !shallowEqualFile(prev, merged)) { newFiles[f.id] = merged; changed = true; }
73
73
  if (!newOrder.includes(f.id)) { newOrder.unshift(f.id); changed = true; }
74
74
  });
75
- if (!changed) return {} as any;
75
+ if (!changed) return state;
76
76
  return { files: newFiles, order: newOrder };
77
77
  }),
78
78
  addFile: (file, opts) => set(state => {
79
79
  const prepend = opts?.prepend !== false; // default true
80
80
  if (state.files[file.id]) {
81
- if (shallowEqualFile(state.files[file.id], file)) return {} as any;
81
+ if (shallowEqualFile(state.files[file.id], file)) return state;
82
82
  return { files: { ...state.files, [file.id]: file } };
83
83
  }
84
84
  return {
@@ -88,13 +88,13 @@ export const useFileStore = create<FileState>((set, get) => ({
88
88
  }),
89
89
  updateFile: (id, patch) => set(state => {
90
90
  const existing = state.files[id];
91
- if (!existing) return {} as any;
91
+ if (!existing) return state;
92
92
  const updated = { ...existing, ...patch } as FileMetadata;
93
- if (shallowEqualFile(existing, updated)) return {} as any;
93
+ if (shallowEqualFile(existing, updated)) return state;
94
94
  return { files: { ...state.files, [id]: updated } };
95
95
  }),
96
96
  removeFile: (id) => set(state => {
97
- if (!state.files[id]) return {} as any;
97
+ if (!state.files[id]) return state;
98
98
  const { [id]: _removed, ...rest } = state.files;
99
99
  const newOrder = state.order.filter(fid => fid !== id);
100
100
  return { files: rest, order: newOrder };