@oxyhq/services 5.4.8 → 5.5.1

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 (154) hide show
  1. package/lib/commonjs/core/index.js +0 -59
  2. package/lib/commonjs/core/index.js.map +1 -1
  3. package/lib/commonjs/index.js +174 -17
  4. package/lib/commonjs/index.js.map +1 -1
  5. package/lib/commonjs/ui/components/FollowButton.js +8 -23
  6. package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
  7. package/lib/commonjs/ui/components/OxyProvider.js +49 -38
  8. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  9. package/lib/commonjs/ui/components/OxySignInButton.js +2 -8
  10. package/lib/commonjs/ui/components/OxySignInButton.js.map +1 -1
  11. package/lib/commonjs/ui/hooks/index.js +15 -2
  12. package/lib/commonjs/ui/hooks/index.js.map +1 -1
  13. package/lib/commonjs/ui/hooks/useAuthFetch.js +182 -0
  14. package/lib/commonjs/ui/hooks/useAuthFetch.js.map +1 -0
  15. package/lib/commonjs/ui/hooks/useFollow.js +10 -29
  16. package/lib/commonjs/ui/hooks/useFollow.js.map +1 -1
  17. package/lib/commonjs/ui/hooks/useOxyFollow.js +190 -0
  18. package/lib/commonjs/ui/hooks/useOxyFollow.js.map +1 -0
  19. package/lib/commonjs/ui/index.js +183 -0
  20. package/lib/commonjs/ui/index.js.map +1 -1
  21. package/lib/commonjs/ui/screens/AccountCenterScreen.js +18 -14
  22. package/lib/commonjs/ui/screens/AccountCenterScreen.js.map +1 -1
  23. package/lib/commonjs/ui/screens/AppInfoScreen.js +37 -19
  24. package/lib/commonjs/ui/screens/AppInfoScreen.js.map +1 -1
  25. package/lib/commonjs/ui/screens/FileManagementScreen.js +27 -9
  26. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  27. package/lib/commonjs/ui/screens/karma/KarmaRewardsScreen.js +2 -8
  28. package/lib/commonjs/ui/screens/karma/KarmaRewardsScreen.js.map +1 -1
  29. package/lib/commonjs/ui/store/index.js +51 -255
  30. package/lib/commonjs/ui/store/index.js.map +1 -1
  31. package/lib/commonjs/ui/store/setupOxyStore.js +63 -0
  32. package/lib/commonjs/ui/store/setupOxyStore.js.map +1 -0
  33. package/lib/commonjs/ui/store/slices/authSlice.js +56 -0
  34. package/lib/commonjs/ui/store/slices/authSlice.js.map +1 -0
  35. package/lib/commonjs/ui/store/slices/followSlice.js +238 -0
  36. package/lib/commonjs/ui/store/slices/followSlice.js.map +1 -0
  37. package/lib/commonjs/ui/store/slices/index.js +129 -0
  38. package/lib/commonjs/ui/store/slices/index.js.map +1 -0
  39. package/lib/commonjs/ui/store/slices/types.js +19 -0
  40. package/lib/commonjs/ui/store/slices/types.js.map +1 -0
  41. package/lib/commonjs/ui/styles/index.js +11 -0
  42. package/lib/commonjs/ui/styles/index.js.map +1 -1
  43. package/lib/commonjs/ui/styles/shadows.js +123 -0
  44. package/lib/commonjs/ui/styles/shadows.js.map +1 -0
  45. package/lib/module/core/index.js +0 -59
  46. package/lib/module/core/index.js.map +1 -1
  47. package/lib/module/index.js +14 -10
  48. package/lib/module/index.js.map +1 -1
  49. package/lib/module/ui/components/FollowButton.js +8 -23
  50. package/lib/module/ui/components/FollowButton.js.map +1 -1
  51. package/lib/module/ui/components/OxyProvider.js +49 -38
  52. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  53. package/lib/module/ui/components/OxySignInButton.js +2 -8
  54. package/lib/module/ui/components/OxySignInButton.js.map +1 -1
  55. package/lib/module/ui/hooks/index.js +2 -1
  56. package/lib/module/ui/hooks/index.js.map +1 -1
  57. package/lib/module/ui/hooks/useAuthFetch.js +177 -0
  58. package/lib/module/ui/hooks/useAuthFetch.js.map +1 -0
  59. package/lib/module/ui/hooks/useFollow.js +10 -29
  60. package/lib/module/ui/hooks/useFollow.js.map +1 -1
  61. package/lib/module/ui/hooks/useOxyFollow.js +186 -0
  62. package/lib/module/ui/hooks/useOxyFollow.js.map +1 -0
  63. package/lib/module/ui/index.js +12 -2
  64. package/lib/module/ui/index.js.map +1 -1
  65. package/lib/module/ui/screens/AccountCenterScreen.js +5 -1
  66. package/lib/module/ui/screens/AccountCenterScreen.js.map +1 -1
  67. package/lib/module/ui/screens/AppInfoScreen.js +37 -19
  68. package/lib/module/ui/screens/AppInfoScreen.js.map +1 -1
  69. package/lib/module/ui/screens/FileManagementScreen.js +27 -9
  70. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  71. package/lib/module/ui/screens/karma/KarmaRewardsScreen.js +2 -8
  72. package/lib/module/ui/screens/karma/KarmaRewardsScreen.js.map +1 -1
  73. package/lib/module/ui/store/index.js +23 -249
  74. package/lib/module/ui/store/index.js.map +1 -1
  75. package/lib/module/ui/store/setupOxyStore.js +59 -0
  76. package/lib/module/ui/store/setupOxyStore.js.map +1 -0
  77. package/lib/module/ui/store/slices/authSlice.js +48 -0
  78. package/lib/module/ui/store/slices/authSlice.js.map +1 -0
  79. package/lib/module/ui/store/slices/followSlice.js +232 -0
  80. package/lib/module/ui/store/slices/followSlice.js.map +1 -0
  81. package/lib/module/ui/store/slices/index.js +11 -0
  82. package/lib/module/ui/store/slices/index.js.map +1 -0
  83. package/lib/module/ui/store/slices/types.js +15 -0
  84. package/lib/module/ui/store/slices/types.js.map +1 -0
  85. package/lib/module/ui/styles/index.js +1 -0
  86. package/lib/module/ui/styles/index.js.map +1 -1
  87. package/lib/module/ui/styles/shadows.js +119 -0
  88. package/lib/module/ui/styles/shadows.js.map +1 -0
  89. package/lib/typescript/core/index.d.ts +0 -28
  90. package/lib/typescript/core/index.d.ts.map +1 -1
  91. package/lib/typescript/index.d.ts +3 -5
  92. package/lib/typescript/index.d.ts.map +1 -1
  93. package/lib/typescript/ui/components/FollowButton.d.ts.map +1 -1
  94. package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
  95. package/lib/typescript/ui/components/OxySignInButton.d.ts.map +1 -1
  96. package/lib/typescript/ui/hooks/index.d.ts +2 -1
  97. package/lib/typescript/ui/hooks/index.d.ts.map +1 -1
  98. package/lib/typescript/ui/hooks/useAuthFetch.d.ts +33 -0
  99. package/lib/typescript/ui/hooks/useAuthFetch.d.ts.map +1 -0
  100. package/lib/typescript/ui/hooks/useFollow.d.ts.map +1 -1
  101. package/lib/typescript/ui/hooks/useOxyFollow.d.ts +81 -0
  102. package/lib/typescript/ui/hooks/useOxyFollow.d.ts.map +1 -0
  103. package/lib/typescript/ui/index.d.ts +3 -1
  104. package/lib/typescript/ui/index.d.ts.map +1 -1
  105. package/lib/typescript/ui/navigation/types.d.ts +22 -4
  106. package/lib/typescript/ui/navigation/types.d.ts.map +1 -1
  107. package/lib/typescript/ui/screens/AccountCenterScreen.d.ts.map +1 -1
  108. package/lib/typescript/ui/screens/AppInfoScreen.d.ts.map +1 -1
  109. package/lib/typescript/ui/screens/FileManagementScreen.d.ts.map +1 -1
  110. package/lib/typescript/ui/screens/karma/KarmaRewardsScreen.d.ts.map +1 -1
  111. package/lib/typescript/ui/store/index.d.ts +19 -58
  112. package/lib/typescript/ui/store/index.d.ts.map +1 -1
  113. package/lib/typescript/ui/store/setupOxyStore.d.ts +29 -0
  114. package/lib/typescript/ui/store/setupOxyStore.d.ts.map +1 -0
  115. package/lib/typescript/ui/store/slices/authSlice.d.ts +32 -0
  116. package/lib/typescript/ui/store/slices/authSlice.d.ts.map +1 -0
  117. package/lib/typescript/ui/store/slices/followSlice.d.ts +120 -0
  118. package/lib/typescript/ui/store/slices/followSlice.d.ts.map +1 -0
  119. package/lib/typescript/ui/store/slices/index.d.ts +9 -0
  120. package/lib/typescript/ui/store/slices/index.d.ts.map +1 -0
  121. package/lib/typescript/ui/store/slices/types.d.ts +16 -0
  122. package/lib/typescript/ui/store/slices/types.d.ts.map +1 -0
  123. package/lib/typescript/ui/styles/index.d.ts +1 -0
  124. package/lib/typescript/ui/styles/index.d.ts.map +1 -1
  125. package/lib/typescript/ui/styles/shadows.d.ts +233 -0
  126. package/lib/typescript/ui/styles/shadows.d.ts.map +1 -0
  127. package/package.json +14 -15
  128. package/src/__tests__/ui/hooks/useOxyFollow.test.tsx +92 -0
  129. package/src/__tests__/ui/store/setupOxyStore.test.ts +50 -0
  130. package/src/__tests__/validate-structure.js +91 -0
  131. package/src/__tests__/validation.js +42 -0
  132. package/src/core/index.ts +0 -66
  133. package/src/index.ts +36 -4
  134. package/src/ui/components/FollowButton.tsx +11 -25
  135. package/src/ui/components/OxyProvider.tsx +48 -33
  136. package/src/ui/components/OxySignInButton.tsx +2 -6
  137. package/src/ui/hooks/index.ts +2 -1
  138. package/src/ui/hooks/useAuthFetch.ts +200 -0
  139. package/src/ui/hooks/useFollow.ts +10 -30
  140. package/src/ui/hooks/useOxyFollow.ts +188 -0
  141. package/src/ui/index.ts +34 -2
  142. package/src/ui/navigation/types.ts +24 -4
  143. package/src/ui/screens/AccountCenterScreen.tsx +5 -7
  144. package/src/ui/screens/AppInfoScreen.tsx +40 -23
  145. package/src/ui/screens/FileManagementScreen.tsx +268 -248
  146. package/src/ui/screens/karma/KarmaRewardsScreen.tsx +2 -5
  147. package/src/ui/store/index.ts +31 -245
  148. package/src/ui/store/setupOxyStore.ts +58 -0
  149. package/src/ui/store/slices/authSlice.ts +43 -0
  150. package/src/ui/store/slices/followSlice.ts +207 -0
  151. package/src/ui/store/slices/index.ts +31 -0
  152. package/src/ui/store/slices/types.ts +33 -0
  153. package/src/ui/styles/index.ts +1 -0
  154. package/src/ui/styles/shadows.ts +112 -0
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { View, Text, StyleSheet, ScrollView } from 'react-native';
3
3
  import { BaseScreenProps } from '../../navigation/types';
4
+ import { shadows } from '../../styles';
4
5
 
5
6
  const KarmaRewardsScreen: React.FC<BaseScreenProps> = ({ goBack, theme }) => {
6
7
  const isDarkTheme = theme === 'dark';
@@ -41,11 +42,7 @@ const styles = StyleSheet.create({
41
42
  borderRadius: 16,
42
43
  padding: 18,
43
44
  marginBottom: 18,
44
- shadowColor: '#000',
45
- shadowOpacity: 0.04,
46
- shadowOffset: { width: 0, height: 1 },
47
- shadowRadius: 4,
48
- elevation: 1,
45
+ ...shadows.small,
49
46
  },
50
47
  rewardTitle: { fontSize: 18, fontWeight: 'bold', marginBottom: 6 },
51
48
  rewardDesc: { fontSize: 15 },
@@ -1,250 +1,36 @@
1
- import { configureStore, createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
2
- import type { User } from '../../models/interfaces';
3
-
4
- interface AuthState {
5
- user: User | null;
6
- isAuthenticated: boolean;
7
- isLoading: boolean;
8
- error: string | null;
9
- }
10
-
11
- interface FollowState {
12
- // Track follow status for each user ID
13
- followingUsers: Record<string, boolean>;
14
- // Track loading state for each user ID
15
- loadingUsers: Record<string, boolean>;
16
- // Track which user IDs are currently being fetched (to prevent duplicate requests)
17
- fetchingUsers: Record<string, boolean>;
18
- // Track any follow/unfollow errors
19
- errors: Record<string, string | null>;
20
- }
21
-
22
- const initialAuthState: AuthState = {
23
- user: null,
24
- isAuthenticated: false,
25
- isLoading: false,
26
- error: null,
27
- };
28
-
29
- const initialFollowState: FollowState = {
30
- followingUsers: {},
31
- loadingUsers: {},
32
- fetchingUsers: {},
33
- errors: {},
34
- };
35
-
36
- // Async thunk for fetching follow status from backend with deduplication
37
- export const fetchFollowStatus = createAsyncThunk(
38
- 'follow/fetchFollowStatus',
39
- async ({ userId, oxyServices }: { userId: string; oxyServices: any }, { rejectWithValue }) => {
40
- try {
41
- // Use the proper core service method
42
- const response = await oxyServices.getFollowStatus(userId);
43
- return { userId, isFollowing: response.isFollowing };
44
- } catch (error: any) {
45
- // Ignore authentication errors when user isn't signed in - don't update state
46
- if (error?.status === 401 || error?.message?.includes('Authentication')) {
47
- return rejectWithValue('Not authenticated');
48
- }
49
- // Log other failures and reject to not update state
50
- console.warn(`Failed to fetch follow status for user ${userId}:`, error);
51
- return rejectWithValue(error?.message || 'Failed to fetch follow status');
52
- }
53
- },
54
- {
55
- // Prevent duplicate requests for the same user ID
56
- condition: ({ userId }, { getState }) => {
57
- const state = getState() as RootState;
58
- // Defensive check for follow state
59
- if (!state.follow?.fetchingUsers) {
60
- return true; // Allow request if state not initialized
61
- }
62
-
63
- const isAlreadyFetching = state.follow.fetchingUsers[userId];
64
-
65
- if (isAlreadyFetching) {
66
- console.log(`⚡ Deduplicating fetch request for user ${userId} - already in progress`);
67
- return false; // Cancel this request
68
- }
69
-
70
- return true; // Allow this request
71
- }
72
- }
73
- );
74
-
75
- // Async thunk for following/unfollowing users using core services
76
- export const toggleFollowUser = createAsyncThunk(
77
- 'follow/toggleFollowUser',
78
- async ({ userId, oxyServices, isCurrentlyFollowing }: {
79
- userId: string;
80
- oxyServices: any;
81
- isCurrentlyFollowing: boolean;
82
- }, { rejectWithValue, dispatch }) => {
83
- try {
84
- let response: { success?: boolean; message?: string; action?: string };
85
- let newFollowState: boolean;
86
-
87
- if (isCurrentlyFollowing) {
88
- // Use the core service to unfollow user
89
- response = await oxyServices.unfollowUser(userId);
90
- newFollowState = false;
91
- } else {
92
- // Use the core service to follow user
93
- response = await oxyServices.followUser(userId);
94
- newFollowState = true;
95
- }
96
-
97
- // Check if the response indicates success (different APIs might return different formats)
98
- const isSuccess = response.success !== false && response.action !== 'error';
99
-
100
- if (isSuccess) {
101
- return {
102
- userId,
103
- isFollowing: newFollowState,
104
- message: response.message || `Successfully ${newFollowState ? 'followed' : 'unfollowed'} user`
105
- };
106
- } else {
107
- return rejectWithValue(response.message || `Failed to ${newFollowState ? 'follow' : 'unfollow'} user`);
108
- }
109
- } catch (error: any) {
110
- // Enhanced error handling with state mismatch detection
111
- let errorMessage = 'Network error occurred';
112
-
113
- if (error?.message) {
114
- errorMessage = error.message;
115
- } else if (error?.response?.data?.message) {
116
- errorMessage = error.response.data.message;
117
- } else if (error?.data?.message) {
118
- errorMessage = error.data.message;
119
- }
120
-
121
- // Handle state mismatch errors by syncing with backend
122
- if (errorMessage.includes('Not following this user') && isCurrentlyFollowing) {
123
- console.warn(`State mismatch detected for user ${userId}: Frontend thinks following, backend says not following. Syncing state...`);
124
- // Auto-sync with backend state
125
- try {
126
- const actualStatus = await oxyServices.getFollowStatus(userId);
127
- dispatch({ type: 'follow/setFollowingStatus', payload: { userId, isFollowing: actualStatus.isFollowing } });
128
- return rejectWithValue('State synced with backend. Please try again.');
129
- } catch (syncError) {
130
- console.error('Failed to sync state with backend:', syncError);
131
- }
132
- } else if (errorMessage.includes('Already following this user') && !isCurrentlyFollowing) {
133
- console.warn(`State mismatch detected for user ${userId}: Frontend thinks not following, backend says following. Syncing state...`);
134
- // Auto-sync with backend state
135
- try {
136
- const actualStatus = await oxyServices.getFollowStatus(userId);
137
- dispatch({ type: 'follow/setFollowingStatus', payload: { userId, isFollowing: actualStatus.isFollowing } });
138
- return rejectWithValue('State synced with backend. Please try again.');
139
- } catch (syncError) {
140
- console.error('Failed to sync state with backend:', syncError);
141
- }
142
- }
143
-
144
- return rejectWithValue(errorMessage);
145
- }
146
- }
147
- );
148
-
149
- const authSlice = createSlice({
150
- name: 'auth',
151
- initialState: initialAuthState,
152
- reducers: {
153
- loginStart(state: AuthState) {
154
- state.isLoading = true;
155
- state.error = null;
156
- },
157
- loginSuccess(state: AuthState, action: PayloadAction<User>) {
158
- state.isLoading = false;
159
- state.isAuthenticated = true;
160
- state.user = action.payload;
161
- },
162
- loginFailure(state: AuthState, action: PayloadAction<string>) {
163
- state.isLoading = false;
164
- state.error = action.payload;
165
- },
166
- logout(state: AuthState) {
167
- state.user = null;
168
- state.isAuthenticated = false;
169
- },
170
- },
171
- });
172
-
173
- const followSlice = createSlice({
174
- name: 'follow',
175
- initialState: initialFollowState,
176
- reducers: {
177
- setFollowingStatus(state: FollowState, action: PayloadAction<{ userId: string; isFollowing: boolean }>) {
178
- const { userId, isFollowing } = action.payload;
179
- state.followingUsers[userId] = isFollowing;
180
- state.errors[userId] = null;
181
- },
182
- clearFollowError(state: FollowState, action: PayloadAction<string>) {
183
- const userId = action.payload;
184
- state.errors[userId] = null;
185
- },
186
- resetFollowState(state: FollowState) {
187
- state.followingUsers = {};
188
- state.loadingUsers = {};
189
- state.fetchingUsers = {};
190
- state.errors = {};
191
- },
192
- },
193
- extraReducers: (builder) => {
194
- builder
195
- // Handle fetchFollowStatus
196
- .addCase(fetchFollowStatus.pending, (state, action) => {
197
- const { userId } = action.meta.arg;
198
- state.fetchingUsers[userId] = true;
199
- state.errors[userId] = null;
200
- })
201
- .addCase(fetchFollowStatus.fulfilled, (state, action) => {
202
- const { userId, isFollowing } = action.payload;
203
- state.followingUsers[userId] = isFollowing;
204
- state.fetchingUsers[userId] = false;
205
- state.errors[userId] = null;
206
- })
207
- .addCase(fetchFollowStatus.rejected, (state, action) => {
208
- const { userId } = action.meta.arg;
209
- state.fetchingUsers[userId] = false;
210
- // Don't update follow state on fetch errors - preserve existing/initial state
211
- if (action.payload !== 'Not authenticated') {
212
- console.warn(`Failed to fetch follow status for user ${userId}:`, action.payload);
213
- }
214
- })
215
- // Handle toggleFollowUser
216
- .addCase(toggleFollowUser.pending, (state, action) => {
217
- const { userId } = action.meta.arg;
218
- state.loadingUsers[userId] = true;
219
- state.errors[userId] = null;
220
- })
221
- .addCase(toggleFollowUser.fulfilled, (state, action) => {
222
- const { userId, isFollowing } = action.payload;
223
- state.followingUsers[userId] = isFollowing;
224
- state.loadingUsers[userId] = false;
225
- state.errors[userId] = null;
226
- })
227
- .addCase(toggleFollowUser.rejected, (state, action) => {
228
- const { userId } = action.meta.arg;
229
- state.loadingUsers[userId] = false;
230
- state.errors[userId] = action.error.message || 'Failed to update follow status';
231
- });
232
- },
233
- });
234
-
235
- export const { loginStart, loginSuccess, loginFailure, logout } = authSlice.actions;
236
- export const { setFollowingStatus, clearFollowError, resetFollowState } = followSlice.actions;
237
-
238
- // Selectors for follow state
239
- export const selectIsUserBeingFetched = (state: RootState, userId: string) =>
240
- state.follow?.fetchingUsers?.[userId] ?? false;
241
-
1
+ /**
2
+ * OxyHQ Services Redux Store
3
+ * Framework-agnostic, tree-shakable Redux integration
4
+ *
5
+ * This module exports individual slices, reducers, actions, selectors, and helpers
6
+ * for easy integration into any Redux store without internal dependencies.
7
+ */
8
+
9
+ // Export individual slices and their components
10
+ export * from './slices';
11
+
12
+ // Export setup helper for easy integration
13
+ export { setupOxyStore, oxyReducers } from './setupOxyStore';
14
+
15
+ // Export types for external store integration
16
+ export type { AuthState, FollowState } from './slices/types';
17
+
18
+ // For backward compatibility: create a legacy store instance
19
+ // This will be deprecated in favor of setupOxyStore()
20
+ import { configureStore } from '@reduxjs/toolkit';
21
+ import { setupOxyStore } from './setupOxyStore';
22
+
23
+ /**
24
+ * @deprecated Use setupOxyStore() instead to integrate with your app's store
25
+ * This internal store will be removed in a future version
26
+ */
242
27
  export const store = configureStore({
243
- reducer: {
244
- auth: authSlice.reducer,
245
- follow: followSlice.reducer,
246
- },
28
+ reducer: setupOxyStore(),
247
29
  });
248
30
 
249
31
  export type RootState = ReturnType<typeof store.getState>;
250
32
  export type AppDispatch = typeof store.dispatch;
33
+
34
+ // Legacy selector (deprecated)
35
+ export const selectIsUserBeingFetched = (state: RootState, userId: string) =>
36
+ state.follow.fetchingUsers[userId] ?? false;
@@ -0,0 +1,58 @@
1
+ import { authReducer } from './slices/authSlice';
2
+ import { followReducer } from './slices/followSlice';
3
+
4
+ /**
5
+ * Setup helper for Oxy Store
6
+ * Returns all Oxy reducers ready to be spread into a host app's Redux store
7
+ *
8
+ * Usage:
9
+ * ```ts
10
+ * const store = configureStore({
11
+ * reducer: {
12
+ * ...setupOxyStore(),
13
+ * appSpecificReducer,
14
+ * },
15
+ * });
16
+ * ```
17
+ */
18
+ export function setupOxyStore() {
19
+ return {
20
+ auth: authReducer,
21
+ follow: followReducer,
22
+ };
23
+ }
24
+
25
+ /**
26
+ * Tree-shakable version where you can pick specific reducers
27
+ *
28
+ * Usage:
29
+ * ```ts
30
+ * const store = configureStore({
31
+ * reducer: {
32
+ * ...setupOxyStore.pick('auth'), // Only include auth
33
+ * // or
34
+ * ...setupOxyStore.pick('auth', 'follow'), // Include both
35
+ * appSpecificReducer,
36
+ * },
37
+ * });
38
+ * ```
39
+ */
40
+ setupOxyStore.pick = function(...keys: Array<'auth' | 'follow'>) {
41
+ const allReducers = setupOxyStore();
42
+ const pickedReducers = {} as Partial<ReturnType<typeof setupOxyStore>>;
43
+
44
+ for (const key of keys) {
45
+ if (key in allReducers) {
46
+ // Use bracket notation with explicit typing
47
+ (pickedReducers as Record<string, any>)[key] = allReducers[key];
48
+ }
49
+ }
50
+
51
+ return pickedReducers;
52
+ };
53
+
54
+ // Export individual reducers for maximum flexibility
55
+ export const oxyReducers = {
56
+ auth: authReducer,
57
+ follow: followReducer,
58
+ };
@@ -0,0 +1,43 @@
1
+ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
2
+ import type { User } from '../../../models/interfaces';
3
+ import type { AuthState } from './types';
4
+ import { initialAuthState } from './types';
5
+
6
+ export const authSlice = createSlice({
7
+ name: 'auth',
8
+ initialState: initialAuthState,
9
+ reducers: {
10
+ loginStart(state: AuthState) {
11
+ state.isLoading = true;
12
+ state.error = null;
13
+ },
14
+ loginSuccess(state: AuthState, action: PayloadAction<User>) {
15
+ state.isLoading = false;
16
+ state.isAuthenticated = true;
17
+ state.user = action.payload;
18
+ },
19
+ loginFailure(state: AuthState, action: PayloadAction<string>) {
20
+ state.isLoading = false;
21
+ state.error = action.payload;
22
+ },
23
+ logout(state: AuthState) {
24
+ state.user = null;
25
+ state.isAuthenticated = false;
26
+ },
27
+ },
28
+ });
29
+
30
+ // Export actions
31
+ export const authActions = authSlice.actions;
32
+ export const { loginStart, loginSuccess, loginFailure, logout } = authSlice.actions;
33
+
34
+ // Export selectors
35
+ export const authSelectors = {
36
+ selectUser: (state: { auth: AuthState }) => state.auth.user,
37
+ selectIsAuthenticated: (state: { auth: AuthState }) => state.auth.isAuthenticated,
38
+ selectIsLoading: (state: { auth: AuthState }) => state.auth.isLoading,
39
+ selectError: (state: { auth: AuthState }) => state.auth.error,
40
+ };
41
+
42
+ // Export reducer
43
+ export const authReducer = authSlice.reducer;
@@ -0,0 +1,207 @@
1
+ import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
2
+ import type { FollowState } from './types';
3
+ import { initialFollowState } from './types';
4
+
5
+ // Define thunk state interface for dependency injection
6
+ interface RootState {
7
+ follow: FollowState;
8
+ }
9
+
10
+ // Async thunk for fetching follow status from backend with deduplication
11
+ export const fetchFollowStatus = createAsyncThunk(
12
+ 'follow/fetchFollowStatus',
13
+ async ({ userId, oxyServices }: { userId: string; oxyServices: any }, { rejectWithValue }) => {
14
+ try {
15
+ // Use the proper core service method
16
+ const response = await oxyServices.getFollowStatus(userId);
17
+ return { userId, isFollowing: response.isFollowing };
18
+ } catch (error: any) {
19
+ // Ignore authentication errors when user isn't signed in - don't update state
20
+ if (error?.status === 401 || error?.message?.includes('Authentication')) {
21
+ return rejectWithValue('Not authenticated');
22
+ }
23
+ // Log other failures and reject to not update state
24
+ console.warn(`Failed to fetch follow status for user ${userId}:`, error);
25
+ return rejectWithValue(error?.message || 'Failed to fetch follow status');
26
+ }
27
+ },
28
+ {
29
+ // Prevent duplicate requests for the same user ID
30
+ condition: ({ userId }, { getState }) => {
31
+ const state = getState() as RootState;
32
+ const isAlreadyFetching = state.follow.fetchingUsers[userId];
33
+
34
+ if (isAlreadyFetching) {
35
+ console.log(`⚡ Deduplicating fetch request for user ${userId} - already in progress`);
36
+ return false; // Cancel this request
37
+ }
38
+
39
+ return true; // Allow this request
40
+ }
41
+ }
42
+ );
43
+
44
+ // Async thunk for following/unfollowing users using core services
45
+ export const toggleFollowUser = createAsyncThunk(
46
+ 'follow/toggleFollowUser',
47
+ async ({ userId, oxyServices, isCurrentlyFollowing }: {
48
+ userId: string;
49
+ oxyServices: any;
50
+ isCurrentlyFollowing: boolean;
51
+ }, { rejectWithValue, dispatch }) => {
52
+ try {
53
+ let response: { success?: boolean; message?: string; action?: string };
54
+ let newFollowState: boolean;
55
+
56
+ if (isCurrentlyFollowing) {
57
+ // Use the core service to unfollow user
58
+ response = await oxyServices.unfollowUser(userId);
59
+ newFollowState = false;
60
+ } else {
61
+ // Use the core service to follow user
62
+ response = await oxyServices.followUser(userId);
63
+ newFollowState = true;
64
+ }
65
+
66
+ // Check if the response indicates success (different APIs might return different formats)
67
+ const isSuccess = response.success !== false && response.action !== 'error';
68
+
69
+ if (isSuccess) {
70
+ return {
71
+ userId,
72
+ isFollowing: newFollowState,
73
+ message: response.message || `Successfully ${newFollowState ? 'followed' : 'unfollowed'} user`
74
+ };
75
+ } else {
76
+ return rejectWithValue(response.message || `Failed to ${newFollowState ? 'follow' : 'unfollow'} user`);
77
+ }
78
+ } catch (error: any) {
79
+ // Enhanced error handling with state mismatch detection
80
+ let errorMessage = 'Network error occurred';
81
+
82
+ if (error?.message) {
83
+ errorMessage = error.message;
84
+ } else if (error?.response?.data?.message) {
85
+ errorMessage = error.response.data.message;
86
+ } else if (error?.data?.message) {
87
+ errorMessage = error.data.message;
88
+ }
89
+
90
+ // Handle state mismatch errors by syncing with backend
91
+ if (errorMessage.includes('Not following this user') && isCurrentlyFollowing) {
92
+ console.warn(`State mismatch detected for user ${userId}: Frontend thinks following, backend says not following. Syncing state...`);
93
+ // Auto-sync with backend state
94
+ try {
95
+ const actualStatus = await oxyServices.getFollowStatus(userId);
96
+ dispatch({ type: 'follow/setFollowingStatus', payload: { userId, isFollowing: actualStatus.isFollowing } });
97
+ return rejectWithValue('State synced with backend. Please try again.');
98
+ } catch (syncError) {
99
+ console.error('Failed to sync state with backend:', syncError);
100
+ }
101
+ } else if (errorMessage.includes('Already following this user') && !isCurrentlyFollowing) {
102
+ console.warn(`State mismatch detected for user ${userId}: Frontend thinks not following, backend says following. Syncing state...`);
103
+ // Auto-sync with backend state
104
+ try {
105
+ const actualStatus = await oxyServices.getFollowStatus(userId);
106
+ dispatch({ type: 'follow/setFollowingStatus', payload: { userId, isFollowing: actualStatus.isFollowing } });
107
+ return rejectWithValue('State synced with backend. Please try again.');
108
+ } catch (syncError) {
109
+ console.error('Failed to sync state with backend:', syncError);
110
+ }
111
+ }
112
+
113
+ return rejectWithValue(errorMessage);
114
+ }
115
+ }
116
+ );
117
+
118
+ export const followSlice = createSlice({
119
+ name: 'follow',
120
+ initialState: initialFollowState,
121
+ reducers: {
122
+ setFollowingStatus(state: FollowState, action: PayloadAction<{ userId: string; isFollowing: boolean }>) {
123
+ const { userId, isFollowing } = action.payload;
124
+ state.followingUsers[userId] = isFollowing;
125
+ state.errors[userId] = null;
126
+ },
127
+ clearFollowError(state: FollowState, action: PayloadAction<string>) {
128
+ const userId = action.payload;
129
+ state.errors[userId] = null;
130
+ },
131
+ resetFollowState(state: FollowState) {
132
+ state.followingUsers = {};
133
+ state.loadingUsers = {};
134
+ state.fetchingUsers = {};
135
+ state.errors = {};
136
+ },
137
+ },
138
+ extraReducers: (builder) => {
139
+ builder
140
+ // Handle fetchFollowStatus
141
+ .addCase(fetchFollowStatus.pending, (state, action) => {
142
+ const { userId } = action.meta.arg;
143
+ state.fetchingUsers[userId] = true;
144
+ state.errors[userId] = null;
145
+ })
146
+ .addCase(fetchFollowStatus.fulfilled, (state, action) => {
147
+ const { userId, isFollowing } = action.payload;
148
+ state.followingUsers[userId] = isFollowing;
149
+ state.fetchingUsers[userId] = false;
150
+ state.errors[userId] = null;
151
+ })
152
+ .addCase(fetchFollowStatus.rejected, (state, action) => {
153
+ const { userId } = action.meta.arg;
154
+ state.fetchingUsers[userId] = false;
155
+ // Don't update follow state on fetch errors - preserve existing/initial state
156
+ if (action.payload !== 'Not authenticated') {
157
+ console.warn(`Failed to fetch follow status for user ${userId}:`, action.payload);
158
+ }
159
+ })
160
+ // Handle toggleFollowUser
161
+ .addCase(toggleFollowUser.pending, (state, action) => {
162
+ const { userId } = action.meta.arg;
163
+ state.loadingUsers[userId] = true;
164
+ state.errors[userId] = null;
165
+ })
166
+ .addCase(toggleFollowUser.fulfilled, (state, action) => {
167
+ const { userId, isFollowing } = action.payload;
168
+ state.followingUsers[userId] = isFollowing;
169
+ state.loadingUsers[userId] = false;
170
+ state.errors[userId] = null;
171
+ })
172
+ .addCase(toggleFollowUser.rejected, (state, action) => {
173
+ const { userId } = action.meta.arg;
174
+ state.loadingUsers[userId] = false;
175
+ state.errors[userId] = action.error.message || 'Failed to update follow status';
176
+ });
177
+ },
178
+ });
179
+
180
+ // Export actions
181
+ export const followActions = followSlice.actions;
182
+ export const { setFollowingStatus, clearFollowError, resetFollowState } = followSlice.actions;
183
+
184
+ // Export thunks
185
+ export const followThunks = {
186
+ fetchFollowStatus,
187
+ toggleFollowUser,
188
+ };
189
+
190
+ // Export selectors
191
+ export const followSelectors = {
192
+ selectFollowingUsers: (state: { follow: FollowState }) => state.follow.followingUsers,
193
+ selectLoadingUsers: (state: { follow: FollowState }) => state.follow.loadingUsers,
194
+ selectFetchingUsers: (state: { follow: FollowState }) => state.follow.fetchingUsers,
195
+ selectFollowErrors: (state: { follow: FollowState }) => state.follow.errors,
196
+ selectIsUserFollowed: (state: { follow: FollowState }, userId: string) =>
197
+ state.follow.followingUsers[userId] ?? false,
198
+ selectIsUserLoading: (state: { follow: FollowState }, userId: string) =>
199
+ state.follow.loadingUsers[userId] ?? false,
200
+ selectIsUserBeingFetched: (state: { follow: FollowState }, userId: string) =>
201
+ state.follow.fetchingUsers[userId] ?? false,
202
+ selectUserError: (state: { follow: FollowState }, userId: string) =>
203
+ state.follow.errors[userId] ?? null,
204
+ };
205
+
206
+ // Export reducer
207
+ export const followReducer = followSlice.reducer;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Redux slices exports
3
+ * This file exports individual slices and their components for tree-shaking
4
+ */
5
+
6
+ export {
7
+ authSlice,
8
+ authActions,
9
+ authSelectors,
10
+ authReducer,
11
+ loginStart,
12
+ loginSuccess,
13
+ loginFailure,
14
+ logout
15
+ } from './authSlice';
16
+
17
+ export {
18
+ followSlice,
19
+ followActions,
20
+ followSelectors,
21
+ followThunks,
22
+ followReducer,
23
+ setFollowingStatus,
24
+ clearFollowError,
25
+ resetFollowState,
26
+ fetchFollowStatus,
27
+ toggleFollowUser
28
+ } from './followSlice';
29
+
30
+ export type { AuthState, FollowState } from './types';
31
+ export { initialAuthState, initialFollowState } from './types';
@@ -0,0 +1,33 @@
1
+ import type { User } from '../../../models/interfaces';
2
+
3
+ export interface AuthState {
4
+ user: User | null;
5
+ isAuthenticated: boolean;
6
+ isLoading: boolean;
7
+ error: string | null;
8
+ }
9
+
10
+ export interface FollowState {
11
+ // Track follow status for each user ID
12
+ followingUsers: Record<string, boolean>;
13
+ // Track loading state for each user ID
14
+ loadingUsers: Record<string, boolean>;
15
+ // Track which user IDs are currently being fetched (to prevent duplicate requests)
16
+ fetchingUsers: Record<string, boolean>;
17
+ // Track any follow/unfollow errors
18
+ errors: Record<string, string | null>;
19
+ }
20
+
21
+ export const initialAuthState: AuthState = {
22
+ user: null,
23
+ isAuthenticated: false,
24
+ isLoading: false,
25
+ error: null,
26
+ };
27
+
28
+ export const initialFollowState: FollowState = {
29
+ followingUsers: {},
30
+ loadingUsers: {},
31
+ fetchingUsers: {},
32
+ errors: {},
33
+ };
@@ -1,2 +1,3 @@
1
1
  export * from './fonts';
2
2
  export * from './theme';
3
+ export * from './shadows';