@oxyhq/services 5.4.4 → 5.4.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.
Files changed (99) hide show
  1. package/lib/commonjs/core/index.js +22 -3
  2. package/lib/commonjs/core/index.js.map +1 -1
  3. package/lib/commonjs/index.js +50 -1
  4. package/lib/commonjs/index.js.map +1 -1
  5. package/lib/commonjs/ui/components/FollowButton.js +79 -31
  6. package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
  7. package/lib/commonjs/ui/components/OxySignInButton.js +2 -2
  8. package/lib/commonjs/ui/components/OxySignInButton.js.map +1 -1
  9. package/lib/commonjs/ui/context/OxyContext.js +11 -1
  10. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  11. package/lib/commonjs/ui/hooks/index.js +13 -0
  12. package/lib/commonjs/ui/hooks/index.js.map +1 -0
  13. package/lib/commonjs/ui/hooks/useFollow.js +184 -0
  14. package/lib/commonjs/ui/hooks/useFollow.js.map +1 -0
  15. package/lib/commonjs/ui/index.js +25 -1
  16. package/lib/commonjs/ui/index.js.map +1 -1
  17. package/lib/commonjs/ui/screens/AccountCenterScreen.js +4 -3
  18. package/lib/commonjs/ui/screens/AccountCenterScreen.js.map +1 -1
  19. package/lib/commonjs/ui/screens/AccountOverviewScreen.js +7 -6
  20. package/lib/commonjs/ui/screens/AccountOverviewScreen.js.map +1 -1
  21. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +3 -2
  22. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  23. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +3 -2
  24. package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -1
  25. package/lib/commonjs/ui/screens/AppInfoScreen.js +6 -8
  26. package/lib/commonjs/ui/screens/AppInfoScreen.js.map +1 -1
  27. package/lib/commonjs/ui/screens/SignInScreen.js +1 -1
  28. package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
  29. package/lib/commonjs/ui/screens/karma/KarmaCenterScreen.js +5 -4
  30. package/lib/commonjs/ui/screens/karma/KarmaCenterScreen.js.map +1 -1
  31. package/lib/commonjs/ui/store/index.js +219 -4
  32. package/lib/commonjs/ui/store/index.js.map +1 -1
  33. package/lib/module/core/index.js +22 -3
  34. package/lib/module/core/index.js.map +1 -1
  35. package/lib/module/index.js +6 -2
  36. package/lib/module/index.js.map +1 -1
  37. package/lib/module/ui/components/FollowButton.js +80 -32
  38. package/lib/module/ui/components/FollowButton.js.map +1 -1
  39. package/lib/module/ui/components/OxySignInButton.js +2 -2
  40. package/lib/module/ui/components/OxySignInButton.js.map +1 -1
  41. package/lib/module/ui/context/OxyContext.js +11 -1
  42. package/lib/module/ui/context/OxyContext.js.map +1 -1
  43. package/lib/module/ui/hooks/index.js +4 -0
  44. package/lib/module/ui/hooks/index.js.map +1 -0
  45. package/lib/module/ui/hooks/useFollow.js +180 -0
  46. package/lib/module/ui/hooks/useFollow.js.map +1 -0
  47. package/lib/module/ui/index.js +9 -0
  48. package/lib/module/ui/index.js.map +1 -1
  49. package/lib/module/ui/screens/AccountCenterScreen.js +4 -3
  50. package/lib/module/ui/screens/AccountCenterScreen.js.map +1 -1
  51. package/lib/module/ui/screens/AccountOverviewScreen.js +7 -6
  52. package/lib/module/ui/screens/AccountOverviewScreen.js.map +1 -1
  53. package/lib/module/ui/screens/AccountSettingsScreen.js +3 -2
  54. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  55. package/lib/module/ui/screens/AccountSwitcherScreen.js +3 -2
  56. package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -1
  57. package/lib/module/ui/screens/AppInfoScreen.js +6 -8
  58. package/lib/module/ui/screens/AppInfoScreen.js.map +1 -1
  59. package/lib/module/ui/screens/SignInScreen.js +1 -1
  60. package/lib/module/ui/screens/SignInScreen.js.map +1 -1
  61. package/lib/module/ui/screens/karma/KarmaCenterScreen.js +5 -4
  62. package/lib/module/ui/screens/karma/KarmaCenterScreen.js.map +1 -1
  63. package/lib/module/ui/store/index.js +215 -4
  64. package/lib/module/ui/store/index.js.map +1 -1
  65. package/lib/typescript/core/index.d.ts +16 -3
  66. package/lib/typescript/core/index.d.ts.map +1 -1
  67. package/lib/typescript/index.d.ts +4 -2
  68. package/lib/typescript/index.d.ts.map +1 -1
  69. package/lib/typescript/ui/components/FollowButton.d.ts +1 -0
  70. package/lib/typescript/ui/components/FollowButton.d.ts.map +1 -1
  71. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  72. package/lib/typescript/ui/hooks/index.d.ts +2 -0
  73. package/lib/typescript/ui/hooks/index.d.ts.map +1 -0
  74. package/lib/typescript/ui/hooks/useFollow.d.ts +43 -0
  75. package/lib/typescript/ui/hooks/useFollow.d.ts.map +1 -0
  76. package/lib/typescript/ui/index.d.ts +3 -0
  77. package/lib/typescript/ui/index.d.ts.map +1 -1
  78. package/lib/typescript/ui/screens/AccountCenterScreen.d.ts.map +1 -1
  79. package/lib/typescript/ui/screens/AccountSwitcherScreen.d.ts.map +1 -1
  80. package/lib/typescript/ui/screens/AppInfoScreen.d.ts.map +1 -1
  81. package/lib/typescript/ui/store/index.d.ts +47 -0
  82. package/lib/typescript/ui/store/index.d.ts.map +1 -1
  83. package/package.json +1 -1
  84. package/src/core/index.ts +88 -3
  85. package/src/index.ts +19 -3
  86. package/src/ui/components/FollowButton.tsx +114 -56
  87. package/src/ui/components/OxySignInButton.tsx +2 -2
  88. package/src/ui/context/OxyContext.tsx +12 -2
  89. package/src/ui/hooks/index.ts +1 -0
  90. package/src/ui/hooks/useFollow.ts +173 -0
  91. package/src/ui/index.ts +9 -0
  92. package/src/ui/screens/AccountCenterScreen.tsx +17 -15
  93. package/src/ui/screens/AccountOverviewScreen.tsx +25 -25
  94. package/src/ui/screens/AccountSettingsScreen.tsx +30 -30
  95. package/src/ui/screens/AccountSwitcherScreen.tsx +34 -33
  96. package/src/ui/screens/AppInfoScreen.tsx +153 -155
  97. package/src/ui/screens/SignInScreen.tsx +2 -2
  98. package/src/ui/screens/karma/KarmaCenterScreen.tsx +4 -4
  99. package/src/ui/store/index.ts +197 -3
@@ -1,25 +1,33 @@
1
- import React, { useState, useEffect } from 'react';
2
- import {
3
- TouchableOpacity,
4
- Text,
5
- StyleSheet,
6
- ViewStyle,
7
- TextStyle,
8
- StyleProp,
1
+ import React, { useEffect, useCallback } from 'react';
2
+ import {
3
+ TouchableOpacity,
4
+ Text,
5
+ StyleSheet,
6
+ ViewStyle,
7
+ TextStyle,
8
+ StyleProp,
9
9
  Platform,
10
10
  ActivityIndicator
11
11
  } from 'react-native';
12
- import Animated, {
13
- useSharedValue,
14
- useAnimatedStyle,
15
- withSpring,
12
+ import Animated, {
13
+ useSharedValue,
14
+ useAnimatedStyle,
15
+ withSpring,
16
16
  interpolateColor,
17
- Easing,
18
- withTiming
17
+ Easing,
18
+ withTiming
19
19
  } from 'react-native-reanimated';
20
+ import { useDispatch, useSelector } from 'react-redux';
20
21
  import { useOxy } from '../context/OxyContext';
21
22
  import { fontFamilies } from '../styles/fonts';
22
23
  import { toast } from '../../lib/sonner';
24
+ import {
25
+ toggleFollowUser,
26
+ setFollowingStatus,
27
+ clearFollowError,
28
+ fetchFollowStatus
29
+ } from '../store';
30
+ import type { RootState, AppDispatch } from '../store';
23
31
 
24
32
  export interface FollowButtonProps {
25
33
  /**
@@ -59,7 +67,7 @@ export interface FollowButtonProps {
59
67
  * @default false
60
68
  */
61
69
  disabled?: boolean;
62
-
70
+
63
71
  /**
64
72
  * Whether to show loading indicator during API calls
65
73
  * @default true
@@ -82,6 +90,7 @@ export interface FollowButtonProps {
82
90
 
83
91
  /**
84
92
  * An animated follow button with interactive state changes and preventDefault support
93
+ * Uses Redux for state management to ensure all buttons with the same user ID stay synchronized
85
94
  *
86
95
  * @example
87
96
  * ```tsx
@@ -130,36 +139,74 @@ const FollowButton: React.FC<FollowButtonProps> = ({
130
139
  preventParentActions = true,
131
140
  onPress,
132
141
  }) => {
142
+ const dispatch = useDispatch();
133
143
  const { oxyServices, isAuthenticated } = useOxy();
134
- const [isFollowing, setIsFollowing] = useState(initiallyFollowing);
135
- const [isLoading, setIsLoading] = useState(false);
144
+
145
+ // Optimized single selector to prevent multiple re-renders
146
+ const followState = useSelector((state: RootState) => ({
147
+ isFollowing: state.follow.followingUsers[userId] ?? initiallyFollowing ?? false,
148
+ isLoading: state.follow.loadingUsers[userId] ?? false,
149
+ error: state.follow.errors[userId]
150
+ }));
151
+
152
+ // Whether the follow status has been loaded from the store
153
+ const isStatusKnown = useSelector((state: RootState) =>
154
+ Object.prototype.hasOwnProperty.call(state.follow.followingUsers, userId)
155
+ );
156
+
157
+ const { isFollowing, isLoading, error } = followState;
136
158
 
137
159
  // Animation values
138
- const animationProgress = useSharedValue(initiallyFollowing ? 1 : 0);
160
+ const animationProgress = useSharedValue(isFollowing ? 1 : 0);
139
161
  const scale = useSharedValue(1);
140
-
162
+
163
+ // Initialize Redux state with initial value if not already set
164
+ useEffect(() => {
165
+ if (userId && !isStatusKnown) {
166
+ // Set the initial state regardless of whether initiallyFollowing is defined
167
+ const initialState = initiallyFollowing ?? false;
168
+ dispatch(setFollowingStatus({ userId, isFollowing: initialState }));
169
+ }
170
+ }, [userId, initiallyFollowing, isStatusKnown, dispatch]);
171
+
172
+ // Fetch latest follow status from backend on mount if authenticated
173
+ // This runs separately and will overwrite the initial state with actual data
174
+ useEffect(() => {
175
+ if (userId && isAuthenticated) {
176
+ dispatch(fetchFollowStatus({ userId, oxyServices }));
177
+ }
178
+ }, [userId, oxyServices, isAuthenticated, dispatch]);
179
+
141
180
  // Update the animation value when isFollowing changes
142
181
  useEffect(() => {
143
182
  animationProgress.value = withTiming(isFollowing ? 1 : 0, {
144
183
  duration: 300,
145
184
  easing: Easing.bezier(0.25, 0.1, 0.25, 1),
146
185
  });
147
- }, [isFollowing, animationProgress]);
186
+ }, [isFollowing]); // Removed animationProgress from dependencies as it's stable
187
+
188
+ // Show error toast when error occurs
189
+ useEffect(() => {
190
+ if (error) {
191
+ toast.error(error);
192
+ dispatch(clearFollowError(userId));
193
+ }
194
+ }, [error]); // Removed userId and dispatch to prevent unnecessary runs
148
195
 
149
- // The button press handler with preventDefault support
150
- const handlePress = async (event?: any) => {
196
+ // The button press handler with preventDefault support - memoized to prevent recreation
197
+ const handlePress = useCallback(async (event?: any) => {
151
198
  // Prevent parent actions if enabled (e.g., if inside a link or pressable container)
152
199
  if (preventParentActions && event) {
153
200
  // For React Native Web compatibility
154
201
  if (Platform.OS === 'web' && event.preventDefault) {
155
202
  event.preventDefault();
156
203
  }
157
-
204
+
158
205
  // Stop event propagation to prevent parent TouchableOpacity/Pressable actions
159
206
  if (event.stopPropagation) {
160
207
  event.stopPropagation();
161
208
  }
162
-
209
+
163
210
  // For React Native, prevent gesture bubbling
164
211
  if (event.nativeEvent && event.nativeEvent.stopPropagation) {
165
212
  event.nativeEvent.stopPropagation();
@@ -172,47 +219,58 @@ const FollowButton: React.FC<FollowButtonProps> = ({
172
219
  return;
173
220
  }
174
221
 
175
- if (disabled || isLoading || !isAuthenticated) return;
176
-
222
+ if (disabled || followState.isLoading) return;
223
+
224
+ // Check if user is authenticated - show toast instead of disabling
225
+ if (!isAuthenticated) {
226
+ toast.error('Please sign in to follow users');
227
+ return;
228
+ }
229
+
177
230
  // Touch feedback animation
178
231
  scale.value = withSpring(0.95, { damping: 10 }, () => {
179
232
  scale.value = withSpring(1);
180
233
  });
181
234
 
182
- setIsLoading(true);
183
-
184
235
  try {
185
- // This should be replaced with actual API call to your services
186
- if (isFollowing) {
187
- // Unfollow API call would go here
188
- // await oxyServices.user.unfollowUser(userId);
189
- console.log(`Unfollowing user: ${userId}`);
190
- await new Promise(resolve => setTimeout(resolve, 500)); // Simulating API call
191
- } else {
192
- // Follow API call would go here
193
- // await oxyServices.user.followUser(userId);
194
- console.log(`Following user: ${userId}`);
195
- await new Promise(resolve => setTimeout(resolve, 500)); // Simulating API call
196
- }
236
+ // Dispatch the async action to follow/unfollow
237
+ const result = await dispatch(toggleFollowUser({
238
+ userId,
239
+ oxyServices,
240
+ isCurrentlyFollowing: followState.isFollowing
241
+ })).unwrap();
197
242
 
198
- // Toggle following state with animation
199
- const newFollowingState = !isFollowing;
200
- setIsFollowing(newFollowingState);
201
-
202
243
  // Call the callback if provided
203
244
  if (onFollowChange) {
204
- onFollowChange(newFollowingState);
245
+ onFollowChange(result.isFollowing);
205
246
  }
206
247
 
207
248
  // Show success toast
208
- toast.success(newFollowingState ? 'Following user!' : 'Unfollowed user');
209
- } catch (error) {
249
+ toast.success(result.isFollowing ? 'Following user!' : 'Unfollowed user');
250
+ } catch (error: any) {
210
251
  console.error('Follow action failed:', error);
211
- toast.error('Failed to update follow status. Please try again.');
212
- } finally {
213
- setIsLoading(false);
252
+
253
+ // Show user-friendly error messages for state mismatches
254
+ const errorMessage = error?.toString() || 'Unknown error';
255
+ if (errorMessage.includes('State synced with backend')) {
256
+ toast.info('Status updated. Please try again.');
257
+ } else {
258
+ toast.error(`Failed to ${followState.isFollowing ? 'unfollow' : 'follow'} user. Please try again.`);
259
+ }
214
260
  }
215
- };
261
+ }, [
262
+ preventParentActions,
263
+ onPress,
264
+ disabled,
265
+ followState.isLoading,
266
+ followState.isFollowing,
267
+ isAuthenticated,
268
+ scale,
269
+ dispatch,
270
+ userId,
271
+ oxyServices,
272
+ onFollowChange
273
+ ]);
216
274
 
217
275
  // Animated styles for the button
218
276
  const animatedButtonStyle = useAnimatedStyle(() => {
@@ -221,7 +279,7 @@ const FollowButton: React.FC<FollowButtonProps> = ({
221
279
  [0, 1],
222
280
  ['#d169e5', '#FFFFFF']
223
281
  );
224
-
282
+
225
283
  const borderColor = interpolateColor(
226
284
  animationProgress.value,
227
285
  [0, 1],
@@ -296,7 +354,7 @@ const FollowButton: React.FC<FollowButtonProps> = ({
296
354
  <TouchableOpacity
297
355
  activeOpacity={0.8}
298
356
  onPress={handlePress}
299
- disabled={disabled || isLoading || !isAuthenticated}
357
+ disabled={disabled || isLoading}
300
358
  >
301
359
  <Animated.View
302
360
  style={[
@@ -307,9 +365,9 @@ const FollowButton: React.FC<FollowButtonProps> = ({
307
365
  ]}
308
366
  >
309
367
  {isLoading && showLoadingState ? (
310
- <ActivityIndicator
311
- size="small"
312
- color={isFollowing ? '#d169e5' : '#FFFFFF'}
368
+ <ActivityIndicator
369
+ size="small"
370
+ color={isFollowing ? '#d169e5' : '#FFFFFF'}
313
371
  />
314
372
  ) : (
315
373
  <Animated.Text
@@ -88,10 +88,10 @@ export const OxySignInButton: React.FC<OxySignInButtonProps> = ({
88
88
  screen = 'SignIn',
89
89
  }) => {
90
90
  // Get all needed values from context in a single call
91
- const { user, showBottomSheet } = useOxy();
91
+ const { isAuthenticated, showBottomSheet } = useOxy();
92
92
 
93
93
  // Don't show the button if already authenticated (unless explicitly overridden)
94
- if (user && !showWhenAuthenticated) return null;
94
+ if (isAuthenticated && !showWhenAuthenticated) return null;
95
95
 
96
96
  // Default handler that uses the context methods
97
97
  const handlePress = () => {
@@ -11,7 +11,7 @@ export interface OxyContextState {
11
11
  minimalUser: MinimalUserData | null; // Minimal user data for UI
12
12
  sessions: SecureClientSession[]; // All active sessions
13
13
  activeSessionId: string | null;
14
- isAuthenticated: boolean;
14
+ isAuthenticated: boolean; // Single source of truth for authentication - use this instead of service methods
15
15
  isLoading: boolean;
16
16
  error: string | null;
17
17
 
@@ -640,13 +640,23 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
640
640
  }
641
641
  }, [bottomSheetRef]);
642
642
 
643
+ // Compute comprehensive authentication status
644
+ // This is the single source of truth for authentication across the entire app
645
+ const isAuthenticated = useMemo(() => {
646
+ // User is authenticated if:
647
+ // 1. We have a full user object loaded, OR
648
+ // 2. We have an active session with a valid token
649
+ // This covers both the loaded state and the loading-but-authenticated state
650
+ return !!user || (!!activeSessionId && !!oxyServices?.getCurrentUserId());
651
+ }, [user, activeSessionId, oxyServices]);
652
+
643
653
  // Context value
644
654
  const contextValue: OxyContextState = {
645
655
  user,
646
656
  minimalUser,
647
657
  sessions,
648
658
  activeSessionId,
649
- isAuthenticated: !!user,
659
+ isAuthenticated,
650
660
  isLoading,
651
661
  error,
652
662
  login,
@@ -0,0 +1 @@
1
+ export { useFollow } from './useFollow';
@@ -0,0 +1,173 @@
1
+ import { useDispatch, useSelector } from 'react-redux';
2
+ import { useCallback, useMemo } from 'react';
3
+ import { toggleFollowUser, setFollowingStatus, clearFollowError, fetchFollowStatus } from '../store';
4
+ import type { RootState } from '../store';
5
+ import { useOxy } from '../context/OxyContext';
6
+
7
+ // Memoized selector to prevent unnecessary re-renders
8
+ const createFollowSelector = (userId: string) => (state: RootState) => ({
9
+ isFollowing: state.follow.followingUsers[userId] ?? false,
10
+ isLoading: state.follow.loadingUsers[userId] ?? false,
11
+ error: state.follow.errors[userId] ?? null,
12
+ });
13
+
14
+ // Memoized selector for multiple users
15
+ const createMultipleFollowSelector = (userIds: string[]) => (state: RootState) => {
16
+ const followData: Record<string, { isFollowing: boolean; isLoading: boolean; error: string | null }> = {};
17
+ const followState = state.follow;
18
+
19
+ for (const userId of userIds) {
20
+ followData[userId] = {
21
+ isFollowing: followState.followingUsers[userId] ?? false,
22
+ isLoading: followState.loadingUsers[userId] ?? false,
23
+ error: followState.errors[userId] ?? null,
24
+ };
25
+ }
26
+
27
+ return {
28
+ followData,
29
+ isAnyLoading: userIds.some(uid => followState.loadingUsers[uid]),
30
+ hasAnyError: userIds.some(uid => followState.errors[uid]),
31
+ allFollowing: userIds.every(uid => followState.followingUsers[uid]),
32
+ allNotFollowing: userIds.every(uid => !followState.followingUsers[uid]),
33
+ };
34
+ };
35
+
36
+ /**
37
+ * Custom hook for managing follow/unfollow functionality
38
+ * Optimized to prevent unnecessary re-renders
39
+ * Can handle both single user and multiple users
40
+ */
41
+ export const useFollow = (userId?: string | string[]) => {
42
+ const dispatch = useDispatch();
43
+ const { oxyServices } = useOxy();
44
+
45
+ // Memoize user IDs to prevent recreation on every render
46
+ const userIds = useMemo(() => {
47
+ return Array.isArray(userId) ? userId : userId ? [userId] : [];
48
+ }, [userId]);
49
+
50
+ const isSingleUser = typeof userId === 'string';
51
+
52
+ // Memoize selectors to prevent recreation
53
+ const singleUserSelector = useMemo(() => {
54
+ return isSingleUser && userId ? createFollowSelector(userId) : null;
55
+ }, [isSingleUser, userId]);
56
+
57
+ const multipleUserSelector = useMemo(() => {
58
+ return !isSingleUser ? createMultipleFollowSelector(userIds) : null;
59
+ }, [isSingleUser, userIds]);
60
+
61
+ // Use appropriate selector based on mode
62
+ const singleUserData = useSelector(singleUserSelector || (() => ({ isFollowing: false, isLoading: false, error: null })));
63
+ const multipleUserData = useSelector(multipleUserSelector || (() => ({
64
+ followData: {},
65
+ isAnyLoading: false,
66
+ hasAnyError: false,
67
+ allFollowing: false,
68
+ allNotFollowing: true
69
+ })));
70
+
71
+ // Memoized callbacks to prevent recreation on every render
72
+ const toggleFollow = useCallback(async () => {
73
+ if (!isSingleUser || !userId) throw new Error('toggleFollow is only available for single user mode');
74
+
75
+ try {
76
+ const result = await dispatch(toggleFollowUser({
77
+ userId,
78
+ oxyServices,
79
+ isCurrentlyFollowing: singleUserData.isFollowing
80
+ })).unwrap();
81
+ return result;
82
+ } catch (error) {
83
+ throw error;
84
+ }
85
+ }, [dispatch, userId, oxyServices, singleUserData.isFollowing, isSingleUser]);
86
+
87
+ const setFollowStatus = useCallback((following: boolean) => {
88
+ if (!isSingleUser || !userId) throw new Error('setFollowStatus is only available for single user mode');
89
+ dispatch(setFollowingStatus({ userId, isFollowing: following }));
90
+ }, [dispatch, userId, isSingleUser]);
91
+
92
+ const fetchStatus = useCallback(async () => {
93
+ if (!isSingleUser || !userId) throw new Error('fetchStatus is only available for single user mode');
94
+
95
+ try {
96
+ await dispatch(fetchFollowStatus({ userId, oxyServices })).unwrap();
97
+ } catch (error) {
98
+ console.warn(`Failed to fetch follow status for user ${userId}:`, error);
99
+ }
100
+ }, [dispatch, userId, oxyServices, isSingleUser]);
101
+
102
+ const clearError = useCallback(() => {
103
+ if (!isSingleUser || !userId) throw new Error('clearError is only available for single user mode');
104
+ dispatch(clearFollowError(userId));
105
+ }, [dispatch, userId, isSingleUser]);
106
+
107
+ // Multiple user callbacks
108
+ const toggleFollowForUser = useCallback(async (targetUserId: string) => {
109
+ const currentState = multipleUserData.followData[targetUserId]?.isFollowing ?? false;
110
+ try {
111
+ const result = await dispatch(toggleFollowUser({
112
+ userId: targetUserId,
113
+ oxyServices,
114
+ isCurrentlyFollowing: currentState
115
+ })).unwrap();
116
+ return result;
117
+ } catch (error) {
118
+ throw error;
119
+ }
120
+ }, [dispatch, oxyServices, multipleUserData.followData]);
121
+
122
+ const setFollowStatusForUser = useCallback((targetUserId: string, following: boolean) => {
123
+ dispatch(setFollowingStatus({ userId: targetUserId, isFollowing: following }));
124
+ }, [dispatch]);
125
+
126
+ const fetchStatusForUser = useCallback(async (targetUserId: string) => {
127
+ try {
128
+ await dispatch(fetchFollowStatus({ userId: targetUserId, oxyServices })).unwrap();
129
+ } catch (error) {
130
+ console.warn(`Failed to fetch follow status for user ${targetUserId}:`, error);
131
+ }
132
+ }, [dispatch, oxyServices]);
133
+
134
+ const fetchAllStatuses = useCallback(async () => {
135
+ const promises = userIds.map(uid =>
136
+ dispatch(fetchFollowStatus({ userId: uid, oxyServices })).unwrap().catch((error: any) => {
137
+ console.warn(`Failed to fetch follow status for user ${uid}:`, error);
138
+ })
139
+ );
140
+ await Promise.all(promises);
141
+ }, [dispatch, userIds, oxyServices]);
142
+
143
+ const clearErrorForUser = useCallback((targetUserId: string) => {
144
+ dispatch(clearFollowError(targetUserId));
145
+ }, [dispatch]);
146
+
147
+ // Return appropriate interface based on mode
148
+ if (isSingleUser && userId) {
149
+ return {
150
+ isFollowing: singleUserData.isFollowing,
151
+ isLoading: singleUserData.isLoading,
152
+ error: singleUserData.error,
153
+ toggleFollow,
154
+ setFollowStatus,
155
+ fetchStatus,
156
+ clearError,
157
+ };
158
+ }
159
+
160
+ return {
161
+ followData: multipleUserData.followData,
162
+ toggleFollowForUser,
163
+ setFollowStatusForUser,
164
+ fetchStatusForUser,
165
+ fetchAllStatuses,
166
+ clearErrorForUser,
167
+ // Helper methods
168
+ isAnyLoading: multipleUserData.isAnyLoading,
169
+ hasAnyError: multipleUserData.hasAnyError,
170
+ allFollowing: multipleUserData.allFollowing,
171
+ allNotFollowing: multipleUserData.allNotFollowing,
172
+ };
173
+ };
package/src/ui/index.ts CHANGED
@@ -30,3 +30,12 @@ export { fontFamilies, fontStyles } from './styles/fonts';
30
30
 
31
31
  // Export types for navigation (internal use)
32
32
  export * from './navigation/types';
33
+
34
+ // Hooks
35
+ export { useFollow } from './hooks';
36
+
37
+ // Screens
38
+ export { default as ProfileScreen } from './screens/ProfileScreen';
39
+
40
+ // Navigation
41
+ export { default as OxyRouter } from './navigation/OxyRouter';
@@ -15,12 +15,12 @@ import { packageInfo } from '../../constants/version';
15
15
  import { toast } from '../../lib/sonner';
16
16
  import { Ionicons } from '@expo/vector-icons';
17
17
  import { fontFamilies } from '../styles/fonts';
18
- import {
19
- ProfileCard,
20
- Section,
21
- QuickActions,
22
- GroupedSection,
23
- GroupedItem
18
+ import {
19
+ ProfileCard,
20
+ Section,
21
+ QuickActions,
22
+ GroupedSection,
23
+ GroupedItem
24
24
  } from '../components';
25
25
 
26
26
  const AccountCenterScreen: React.FC<BaseScreenProps> = ({
@@ -28,7 +28,7 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
28
28
  theme,
29
29
  navigate,
30
30
  }) => {
31
- const { user, logout, isLoading, sessions } = useOxy();
31
+ const { user, logout, isLoading, sessions, isAuthenticated } = useOxy();
32
32
 
33
33
  const isDarkTheme = theme === 'dark';
34
34
  const textColor = isDarkTheme ? '#FFFFFF' : '#000000';
@@ -69,7 +69,7 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
69
69
  );
70
70
  };
71
71
 
72
- if (!user) {
72
+ if (!isAuthenticated) {
73
73
  return (
74
74
  <View style={[styles.container, { backgroundColor }]}>
75
75
  <Text style={[styles.message, { color: textColor }]}>Not signed in</Text>
@@ -88,13 +88,15 @@ const AccountCenterScreen: React.FC<BaseScreenProps> = ({
88
88
  return (
89
89
  <View style={[styles.container, { backgroundColor }]}>
90
90
  {/* Header with user profile */}
91
- <ProfileCard
92
- user={user}
93
- theme={theme}
94
- onEditPress={() => navigate('AccountSettings', { activeTab: 'profile' })}
95
- onClosePress={onClose}
96
- showCloseButton={!!onClose}
97
- />
91
+ {user && (
92
+ <ProfileCard
93
+ user={user}
94
+ theme={theme}
95
+ onEditPress={() => navigate('AccountSettings', { activeTab: 'profile' })}
96
+ onClosePress={onClose}
97
+ showCloseButton={!!onClose}
98
+ />
99
+ )}
98
100
 
99
101
  <ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContainer} showsVerticalScrollIndicator={false}>
100
102
  {/* Quick Actions */}