@oxyhq/services 5.4.4 → 5.4.6

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 (105) hide show
  1. package/lib/commonjs/core/index.js +81 -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 +94 -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 +203 -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 +25 -45
  26. package/lib/commonjs/ui/screens/AppInfoScreen.js.map +1 -1
  27. package/lib/commonjs/ui/screens/FileManagementScreen.js +9 -27
  28. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  29. package/lib/commonjs/ui/screens/SignInScreen.js +1 -1
  30. package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
  31. package/lib/commonjs/ui/screens/karma/KarmaCenterScreen.js +5 -4
  32. package/lib/commonjs/ui/screens/karma/KarmaCenterScreen.js.map +1 -1
  33. package/lib/commonjs/ui/store/index.js +223 -4
  34. package/lib/commonjs/ui/store/index.js.map +1 -1
  35. package/lib/module/core/index.js +81 -3
  36. package/lib/module/core/index.js.map +1 -1
  37. package/lib/module/index.js +6 -2
  38. package/lib/module/index.js.map +1 -1
  39. package/lib/module/ui/components/FollowButton.js +95 -32
  40. package/lib/module/ui/components/FollowButton.js.map +1 -1
  41. package/lib/module/ui/components/OxySignInButton.js +2 -2
  42. package/lib/module/ui/components/OxySignInButton.js.map +1 -1
  43. package/lib/module/ui/context/OxyContext.js +11 -1
  44. package/lib/module/ui/context/OxyContext.js.map +1 -1
  45. package/lib/module/ui/hooks/index.js +4 -0
  46. package/lib/module/ui/hooks/index.js.map +1 -0
  47. package/lib/module/ui/hooks/useFollow.js +199 -0
  48. package/lib/module/ui/hooks/useFollow.js.map +1 -0
  49. package/lib/module/ui/index.js +9 -0
  50. package/lib/module/ui/index.js.map +1 -1
  51. package/lib/module/ui/screens/AccountCenterScreen.js +4 -3
  52. package/lib/module/ui/screens/AccountCenterScreen.js.map +1 -1
  53. package/lib/module/ui/screens/AccountOverviewScreen.js +7 -6
  54. package/lib/module/ui/screens/AccountOverviewScreen.js.map +1 -1
  55. package/lib/module/ui/screens/AccountSettingsScreen.js +3 -2
  56. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  57. package/lib/module/ui/screens/AccountSwitcherScreen.js +3 -2
  58. package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -1
  59. package/lib/module/ui/screens/AppInfoScreen.js +25 -45
  60. package/lib/module/ui/screens/AppInfoScreen.js.map +1 -1
  61. package/lib/module/ui/screens/FileManagementScreen.js +9 -27
  62. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  63. package/lib/module/ui/screens/SignInScreen.js +1 -1
  64. package/lib/module/ui/screens/SignInScreen.js.map +1 -1
  65. package/lib/module/ui/screens/karma/KarmaCenterScreen.js +5 -4
  66. package/lib/module/ui/screens/karma/KarmaCenterScreen.js.map +1 -1
  67. package/lib/module/ui/store/index.js +219 -4
  68. package/lib/module/ui/store/index.js.map +1 -1
  69. package/lib/typescript/core/index.d.ts +44 -3
  70. package/lib/typescript/core/index.d.ts.map +1 -1
  71. package/lib/typescript/index.d.ts +4 -2
  72. package/lib/typescript/index.d.ts.map +1 -1
  73. package/lib/typescript/ui/components/FollowButton.d.ts +1 -0
  74. package/lib/typescript/ui/components/FollowButton.d.ts.map +1 -1
  75. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  76. package/lib/typescript/ui/hooks/index.d.ts +2 -0
  77. package/lib/typescript/ui/hooks/index.d.ts.map +1 -0
  78. package/lib/typescript/ui/hooks/useFollow.d.ts +43 -0
  79. package/lib/typescript/ui/hooks/useFollow.d.ts.map +1 -0
  80. package/lib/typescript/ui/index.d.ts +3 -0
  81. package/lib/typescript/ui/index.d.ts.map +1 -1
  82. package/lib/typescript/ui/screens/AccountCenterScreen.d.ts.map +1 -1
  83. package/lib/typescript/ui/screens/AccountSwitcherScreen.d.ts.map +1 -1
  84. package/lib/typescript/ui/screens/AppInfoScreen.d.ts.map +1 -1
  85. package/lib/typescript/ui/screens/FileManagementScreen.d.ts.map +1 -1
  86. package/lib/typescript/ui/store/index.d.ts +47 -0
  87. package/lib/typescript/ui/store/index.d.ts.map +1 -1
  88. package/package.json +1 -1
  89. package/src/core/index.ts +88 -3
  90. package/src/index.ts +19 -3
  91. package/src/ui/components/FollowButton.tsx +128 -56
  92. package/src/ui/components/OxySignInButton.tsx +2 -2
  93. package/src/ui/context/OxyContext.tsx +12 -2
  94. package/src/ui/hooks/index.ts +1 -0
  95. package/src/ui/hooks/useFollow.ts +193 -0
  96. package/src/ui/index.ts +9 -0
  97. package/src/ui/screens/AccountCenterScreen.tsx +17 -15
  98. package/src/ui/screens/AccountOverviewScreen.tsx +25 -25
  99. package/src/ui/screens/AccountSettingsScreen.tsx +30 -30
  100. package/src/ui/screens/AccountSwitcherScreen.tsx +34 -33
  101. package/src/ui/screens/AppInfoScreen.tsx +173 -192
  102. package/src/ui/screens/FileManagementScreen.tsx +248 -268
  103. package/src/ui/screens/SignInScreen.tsx +2 -2
  104. package/src/ui/screens/karma/KarmaCenterScreen.tsx +4 -4
  105. package/src/ui/store/index.ts +202 -3
@@ -5,12 +5,59 @@ interface AuthState {
5
5
  isLoading: boolean;
6
6
  error: string | null;
7
7
  }
8
+ interface FollowState {
9
+ followingUsers: Record<string, boolean>;
10
+ loadingUsers: Record<string, boolean>;
11
+ fetchingUsers: Record<string, boolean>;
12
+ errors: Record<string, string | null>;
13
+ }
14
+ export declare const fetchFollowStatus: import("@reduxjs/toolkit").AsyncThunk<{
15
+ userId: string;
16
+ isFollowing: any;
17
+ }, {
18
+ userId: string;
19
+ oxyServices: any;
20
+ }, {
21
+ state?: unknown;
22
+ dispatch?: import("redux-thunk").ThunkDispatch<unknown, unknown, import("redux").UnknownAction>;
23
+ extra?: unknown;
24
+ rejectValue?: unknown;
25
+ serializedErrorType?: unknown;
26
+ pendingMeta?: unknown;
27
+ fulfilledMeta?: unknown;
28
+ rejectedMeta?: unknown;
29
+ }>;
30
+ export declare const toggleFollowUser: import("@reduxjs/toolkit").AsyncThunk<{
31
+ userId: string;
32
+ isFollowing: boolean;
33
+ message: string;
34
+ }, {
35
+ userId: string;
36
+ oxyServices: any;
37
+ isCurrentlyFollowing: boolean;
38
+ }, {
39
+ state?: unknown;
40
+ dispatch?: import("redux-thunk").ThunkDispatch<unknown, unknown, import("redux").UnknownAction>;
41
+ extra?: unknown;
42
+ rejectValue?: unknown;
43
+ serializedErrorType?: unknown;
44
+ pendingMeta?: unknown;
45
+ fulfilledMeta?: unknown;
46
+ rejectedMeta?: unknown;
47
+ }>;
8
48
  export declare const loginStart: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"auth/loginStart">, loginSuccess: import("@reduxjs/toolkit").ActionCreatorWithPayload<User, "auth/loginSuccess">, loginFailure: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "auth/loginFailure">, logout: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"auth/logout">;
49
+ export declare const setFollowingStatus: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
50
+ userId: string;
51
+ isFollowing: boolean;
52
+ }, "follow/setFollowingStatus">, clearFollowError: import("@reduxjs/toolkit").ActionCreatorWithPayload<string, "follow/clearFollowError">, resetFollowState: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"follow/resetFollowState">;
53
+ export declare const selectIsUserBeingFetched: (state: RootState, userId: string) => boolean;
9
54
  export declare const store: import("@reduxjs/toolkit").EnhancedStore<{
10
55
  auth: AuthState;
56
+ follow: FollowState;
11
57
  }, import("redux").UnknownAction, import("@reduxjs/toolkit").Tuple<[import("redux").StoreEnhancer<{
12
58
  dispatch: import("redux-thunk").ThunkDispatch<{
13
59
  auth: AuthState;
60
+ follow: FollowState;
14
61
  }, undefined, import("redux").UnknownAction>;
15
62
  }>, import("redux").StoreEnhancer]>>;
16
63
  export type RootState = ReturnType<typeof store.getState>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/ui/store/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAEpD,UAAU,SAAS;IACjB,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,eAAe,EAAE,OAAO,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAiCD,eAAO,MAAQ,UAAU,6EAAE,YAAY,kFAAE,YAAY,oFAAE,MAAM,uEAAsB,CAAC;AAEpF,eAAO,MAAM,KAAK;;;;;;oCAIhB,CAAC;AAEH,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,KAAK,CAAC,QAAQ,CAAC,CAAC;AAC1D,MAAM,MAAM,WAAW,GAAG,OAAO,KAAK,CAAC,QAAQ,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/ui/store/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAEpD,UAAU,SAAS;IACjB,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;IAClB,eAAe,EAAE,OAAO,CAAC;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,UAAU,WAAW;IAEnB,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAExC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEtC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC;CACvC;AAiBD,eAAO,MAAM,iBAAiB;;;;YAEc,MAAM;iBAAe,GAAG;;;;;;;;;;EAkCnE,CAAC;AAGF,eAAO,MAAM,gBAAgB;;;;;YAGjB,MAAM;iBACD,GAAG;0BACM,OAAO;;;;;;;;;;EAkEhC,CAAC;AAwFF,eAAO,MAAQ,UAAU,6EAAE,YAAY,kFAAE,YAAY,oFAAE,MAAM,uEAAsB,CAAC;AACpF,eAAO,MAAQ,kBAAkB;YA3D0C,MAAM;iBAAe,OAAO;iCA2DpE,gBAAgB,0FAAE,gBAAgB,mFAAwB,CAAC;AAG9F,eAAO,MAAM,wBAAwB,GAAI,OAAO,SAAS,EAAE,QAAQ,MAAM,YACzB,CAAC;AAEjD,eAAO,MAAM,KAAK;;;;;;;;oCAKhB,CAAC;AAEH,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,KAAK,CAAC,QAAQ,CAAC,CAAC;AAC1D,MAAM,MAAM,WAAW,GAAG,OAAO,KAAK,CAAC,QAAQ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxyhq/services",
3
- "version": "5.4.4",
3
+ "version": "5.4.6",
4
4
  "description": "Reusable OxyHQ module to handle authentication, user management, karma system, device-based session management and more 🚀",
5
5
  "main": "lib/commonjs/node/index.js",
6
6
  "module": "lib/module/node/index.js",
package/src/core/index.ts CHANGED
@@ -67,6 +67,9 @@ interface JwtPayload {
67
67
 
68
68
  /**
69
69
  * OxyServices - Client library for interacting with the Oxy API
70
+ *
71
+ * Note: For authentication status in UI components, use `isAuthenticated` from useOxy() context
72
+ * instead of checking token status directly on this service.
70
73
  */
71
74
  export class OxyServices {
72
75
  private client: AxiosInstance;
@@ -179,10 +182,12 @@ export class OxyServices {
179
182
  }
180
183
 
181
184
  /**
182
- * Checks if the user is currently authenticated
183
- * @returns Boolean indicating authentication status
185
+ * Internal method to check if we have an access token
186
+ * @private
187
+ * @returns Boolean indicating if access token exists
188
+ * @internal - Use `isAuthenticated` from useOxy() context in UI components instead
184
189
  */
185
- public isAuthenticated(): boolean {
190
+ private hasAccessToken(): boolean {
186
191
  return this.accessToken !== null;
187
192
  }
188
193
 
@@ -562,6 +567,20 @@ export class OxyServices {
562
567
  }
563
568
  }
564
569
 
570
+ /**
571
+ * Get follow status for a user
572
+ * @param userId - User ID to check follow status for
573
+ * @returns Whether the current user is following the specified user
574
+ */
575
+ async getFollowStatus(userId: string): Promise<{ isFollowing: boolean }> {
576
+ try {
577
+ const res = await this.client.get(`/users/${userId}/following-status`);
578
+ return res.data;
579
+ } catch (error) {
580
+ throw this.handleError(error);
581
+ }
582
+ }
583
+
565
584
  /**
566
585
  * Get all followers of a user
567
586
  * @param userId - User ID to get followers for
@@ -1657,6 +1676,72 @@ export class OxyServices {
1657
1676
  throw this.handleError(error);
1658
1677
  }
1659
1678
  }
1679
+
1680
+ /**
1681
+ * Health check endpoint to verify API connectivity
1682
+ * @returns Health status and basic server info
1683
+ */
1684
+ async healthCheck(): Promise<{
1685
+ status: string;
1686
+ users?: number;
1687
+ timestamp?: string;
1688
+ [key: string]: any
1689
+ }> {
1690
+ try {
1691
+ const res = await this.client.get('/');
1692
+ return res.data;
1693
+ } catch (error) {
1694
+ throw this.handleError(error);
1695
+ }
1696
+ }
1697
+
1698
+ /**
1699
+ * Download file content using authenticated request
1700
+ * @param fileId - The file ID to download
1701
+ * @returns Response object for further processing
1702
+ */
1703
+ async downloadFileContent(fileId: string): Promise<Response> {
1704
+ try {
1705
+ const downloadUrl = this.getFileDownloadUrl(fileId);
1706
+ const response = await fetch(downloadUrl);
1707
+
1708
+ if (!response.ok) {
1709
+ throw new Error(`Download failed: ${response.status} ${response.statusText}`);
1710
+ }
1711
+
1712
+ return response;
1713
+ } catch (error) {
1714
+ throw this.handleError(error);
1715
+ }
1716
+ }
1717
+
1718
+ /**
1719
+ * Get file content as text using authenticated request
1720
+ * @param fileId - The file ID to get content for
1721
+ * @returns File content as string
1722
+ */
1723
+ async getFileContentAsText(fileId: string): Promise<string> {
1724
+ try {
1725
+ const response = await this.downloadFileContent(fileId);
1726
+ return await response.text();
1727
+ } catch (error) {
1728
+ throw this.handleError(error);
1729
+ }
1730
+ }
1731
+
1732
+ /**
1733
+ * Get file content as blob using authenticated request
1734
+ * @param fileId - The file ID to get content for
1735
+ * @returns File content as blob
1736
+ */
1737
+ async getFileContentAsBlob(fileId: string): Promise<Blob> {
1738
+ try {
1739
+ const response = await this.downloadFileContent(fileId);
1740
+ return await response.blob();
1741
+ } catch (error) {
1742
+ throw this.handleError(error);
1743
+ }
1744
+ }
1660
1745
  }
1661
1746
 
1662
1747
  export default OxyServices;
package/src/index.ts CHANGED
@@ -25,7 +25,13 @@ import {
25
25
  Avatar,
26
26
  FollowButton,
27
27
  FontLoader,
28
- OxyIcon
28
+ OxyIcon,
29
+ useFollow,
30
+ ProfileScreen,
31
+ OxyRouter,
32
+ store,
33
+ type RootState,
34
+ type AppDispatch,
29
35
  } from './ui';
30
36
 
31
37
  // ------------- Type Imports -------------
@@ -58,9 +64,19 @@ export {
58
64
  Avatar,
59
65
  FollowButton,
60
66
  FontLoader,
61
- OxyIcon
67
+ OxyIcon,
68
+ useFollow,
69
+ ProfileScreen,
70
+ OxyRouter,
71
+ store,
72
+ type RootState,
73
+ type AppDispatch,
62
74
  };
63
75
 
64
76
  // ------------- Type Exports -------------
65
77
  export { OxyContextState, OxyContextProviderProps };
66
- export * from './ui/navigation/types';
78
+ export * from './ui/navigation/types';
79
+ export * from './models/secureSession';
80
+
81
+ // Sonner toast integration
82
+ export { toast } from './lib/sonner';
@@ -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,88 @@ 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 with defensive checks
146
+ const followState = useSelector((state: RootState) => {
147
+ // Defensive check to handle cases where follow state might not be initialized yet
148
+ if (!state.follow) {
149
+ return {
150
+ isFollowing: initiallyFollowing ?? false,
151
+ isLoading: false,
152
+ error: null
153
+ };
154
+ }
155
+
156
+ return {
157
+ isFollowing: state.follow.followingUsers?.[userId] ?? initiallyFollowing ?? false,
158
+ isLoading: state.follow.loadingUsers?.[userId] ?? false,
159
+ error: state.follow.errors?.[userId] ?? null
160
+ };
161
+ });
162
+
163
+ // Whether the follow status has been loaded from the store with defensive check
164
+ const isStatusKnown = useSelector((state: RootState) => {
165
+ if (!state.follow?.followingUsers) {
166
+ return false;
167
+ }
168
+ return Object.prototype.hasOwnProperty.call(state.follow.followingUsers, userId);
169
+ });
170
+
171
+ const { isFollowing, isLoading, error } = followState;
136
172
 
137
173
  // Animation values
138
- const animationProgress = useSharedValue(initiallyFollowing ? 1 : 0);
174
+ const animationProgress = useSharedValue(isFollowing ? 1 : 0);
139
175
  const scale = useSharedValue(1);
140
-
176
+
177
+ // Initialize Redux state with initial value if not already set
178
+ useEffect(() => {
179
+ if (userId && !isStatusKnown) {
180
+ // Set the initial state regardless of whether initiallyFollowing is defined
181
+ const initialState = initiallyFollowing ?? false;
182
+ dispatch(setFollowingStatus({ userId, isFollowing: initialState }));
183
+ }
184
+ }, [userId, initiallyFollowing, isStatusKnown, dispatch]);
185
+
186
+ // Fetch latest follow status from backend on mount if authenticated
187
+ // This runs separately and will overwrite the initial state with actual data
188
+ useEffect(() => {
189
+ if (userId && isAuthenticated) {
190
+ dispatch(fetchFollowStatus({ userId, oxyServices }));
191
+ }
192
+ }, [userId, oxyServices, isAuthenticated, dispatch]);
193
+
141
194
  // Update the animation value when isFollowing changes
142
195
  useEffect(() => {
143
196
  animationProgress.value = withTiming(isFollowing ? 1 : 0, {
144
197
  duration: 300,
145
198
  easing: Easing.bezier(0.25, 0.1, 0.25, 1),
146
199
  });
147
- }, [isFollowing, animationProgress]);
200
+ }, [isFollowing]); // Removed animationProgress from dependencies as it's stable
148
201
 
149
- // The button press handler with preventDefault support
150
- const handlePress = async (event?: any) => {
202
+ // Show error toast when error occurs
203
+ useEffect(() => {
204
+ if (error) {
205
+ toast.error(error);
206
+ dispatch(clearFollowError(userId));
207
+ }
208
+ }, [error]); // Removed userId and dispatch to prevent unnecessary runs
209
+
210
+ // The button press handler with preventDefault support - memoized to prevent recreation
211
+ const handlePress = useCallback(async (event?: any) => {
151
212
  // Prevent parent actions if enabled (e.g., if inside a link or pressable container)
152
213
  if (preventParentActions && event) {
153
214
  // For React Native Web compatibility
154
215
  if (Platform.OS === 'web' && event.preventDefault) {
155
216
  event.preventDefault();
156
217
  }
157
-
218
+
158
219
  // Stop event propagation to prevent parent TouchableOpacity/Pressable actions
159
220
  if (event.stopPropagation) {
160
221
  event.stopPropagation();
161
222
  }
162
-
223
+
163
224
  // For React Native, prevent gesture bubbling
164
225
  if (event.nativeEvent && event.nativeEvent.stopPropagation) {
165
226
  event.nativeEvent.stopPropagation();
@@ -172,47 +233,58 @@ const FollowButton: React.FC<FollowButtonProps> = ({
172
233
  return;
173
234
  }
174
235
 
175
- if (disabled || isLoading || !isAuthenticated) return;
176
-
236
+ if (disabled || followState.isLoading) return;
237
+
238
+ // Check if user is authenticated - show toast instead of disabling
239
+ if (!isAuthenticated) {
240
+ toast.error('Please sign in to follow users');
241
+ return;
242
+ }
243
+
177
244
  // Touch feedback animation
178
245
  scale.value = withSpring(0.95, { damping: 10 }, () => {
179
246
  scale.value = withSpring(1);
180
247
  });
181
248
 
182
- setIsLoading(true);
183
-
184
249
  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
- }
250
+ // Dispatch the async action to follow/unfollow
251
+ const result = await dispatch(toggleFollowUser({
252
+ userId,
253
+ oxyServices,
254
+ isCurrentlyFollowing: followState.isFollowing
255
+ })).unwrap();
197
256
 
198
- // Toggle following state with animation
199
- const newFollowingState = !isFollowing;
200
- setIsFollowing(newFollowingState);
201
-
202
257
  // Call the callback if provided
203
258
  if (onFollowChange) {
204
- onFollowChange(newFollowingState);
259
+ onFollowChange(result.isFollowing);
205
260
  }
206
261
 
207
262
  // Show success toast
208
- toast.success(newFollowingState ? 'Following user!' : 'Unfollowed user');
209
- } catch (error) {
263
+ toast.success(result.isFollowing ? 'Following user!' : 'Unfollowed user');
264
+ } catch (error: any) {
210
265
  console.error('Follow action failed:', error);
211
- toast.error('Failed to update follow status. Please try again.');
212
- } finally {
213
- setIsLoading(false);
266
+
267
+ // Show user-friendly error messages for state mismatches
268
+ const errorMessage = error?.toString() || 'Unknown error';
269
+ if (errorMessage.includes('State synced with backend')) {
270
+ toast.info('Status updated. Please try again.');
271
+ } else {
272
+ toast.error(`Failed to ${followState.isFollowing ? 'unfollow' : 'follow'} user. Please try again.`);
273
+ }
214
274
  }
215
- };
275
+ }, [
276
+ preventParentActions,
277
+ onPress,
278
+ disabled,
279
+ followState.isLoading,
280
+ followState.isFollowing,
281
+ isAuthenticated,
282
+ scale,
283
+ dispatch,
284
+ userId,
285
+ oxyServices,
286
+ onFollowChange
287
+ ]);
216
288
 
217
289
  // Animated styles for the button
218
290
  const animatedButtonStyle = useAnimatedStyle(() => {
@@ -221,7 +293,7 @@ const FollowButton: React.FC<FollowButtonProps> = ({
221
293
  [0, 1],
222
294
  ['#d169e5', '#FFFFFF']
223
295
  );
224
-
296
+
225
297
  const borderColor = interpolateColor(
226
298
  animationProgress.value,
227
299
  [0, 1],
@@ -296,7 +368,7 @@ const FollowButton: React.FC<FollowButtonProps> = ({
296
368
  <TouchableOpacity
297
369
  activeOpacity={0.8}
298
370
  onPress={handlePress}
299
- disabled={disabled || isLoading || !isAuthenticated}
371
+ disabled={disabled || isLoading}
300
372
  >
301
373
  <Animated.View
302
374
  style={[
@@ -307,9 +379,9 @@ const FollowButton: React.FC<FollowButtonProps> = ({
307
379
  ]}
308
380
  >
309
381
  {isLoading && showLoadingState ? (
310
- <ActivityIndicator
311
- size="small"
312
- color={isFollowing ? '#d169e5' : '#FFFFFF'}
382
+ <ActivityIndicator
383
+ size="small"
384
+ color={isFollowing ? '#d169e5' : '#FFFFFF'}
313
385
  />
314
386
  ) : (
315
387
  <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';