@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
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Zero Config Authenticated Fetch Hook
3
+ *
4
+ * Simple hook that provides fetch-like API with automatic authentication
5
+ * Leverages the existing useOxy hook and OxyProvider infrastructure
6
+ *
7
+ * Usage:
8
+ * const authFetch = useAuthFetch();
9
+ * const response = await authFetch('/api/protected');
10
+ * const data = await authFetch.get('/api/users');
11
+ */
12
+
13
+ import { useCallback } from 'react';
14
+ import { useOxy } from '../context/OxyContext';
15
+
16
+ export interface AuthFetchOptions extends Omit<RequestInit, 'body'> {
17
+ body?: any; // Allow any type for body, we'll JSON.stringify if needed
18
+ }
19
+
20
+ export interface AuthFetchAPI {
21
+ // Main fetch function (drop-in replacement)
22
+ (input: RequestInfo | URL, init?: AuthFetchOptions): Promise<Response>;
23
+
24
+ // Convenience methods for JSON APIs
25
+ get: (endpoint: string, options?: AuthFetchOptions) => Promise<any>;
26
+ post: (endpoint: string, data?: any, options?: AuthFetchOptions) => Promise<any>;
27
+ put: (endpoint: string, data?: any, options?: AuthFetchOptions) => Promise<any>;
28
+ delete: (endpoint: string, options?: AuthFetchOptions) => Promise<any>;
29
+
30
+ // Access to auth state and methods
31
+ isAuthenticated: boolean;
32
+ user: any;
33
+ login: (username: string, password: string) => Promise<any>;
34
+ logout: () => Promise<void>;
35
+ signUp: (username: string, email: string, password: string) => Promise<any>;
36
+ }
37
+
38
+ /**
39
+ * Hook that provides authenticated fetch functionality
40
+ * Uses the existing OxyServices instance from useOxy context
41
+ */
42
+ export function useAuthFetch(): AuthFetchAPI {
43
+ const { oxyServices, isAuthenticated, user, login, logout, signUp } = useOxy();
44
+
45
+ // Main fetch function with automatic auth headers
46
+ const authFetch = useCallback(async (input: RequestInfo | URL, init?: AuthFetchOptions): Promise<Response> => {
47
+ const url = resolveURL(input, oxyServices.getBaseURL());
48
+ const options = await addAuthHeaders(init, oxyServices);
49
+
50
+ try {
51
+ let response = await fetch(url, options);
52
+
53
+ // Handle token expiry and automatic refresh
54
+ if (response.status === 401 && oxyServices.getCurrentUserId()) {
55
+ // Try to refresh token and retry
56
+ try {
57
+ await oxyServices.refreshTokens();
58
+ const retryOptions = await addAuthHeaders(init, oxyServices);
59
+ response = await fetch(url, retryOptions);
60
+ } catch (refreshError) {
61
+ // Refresh failed, user needs to login again
62
+ console.warn('Token refresh failed, user needs to re-authenticate');
63
+ throw new Error('Authentication expired. Please login again.');
64
+ }
65
+ }
66
+
67
+ return response;
68
+ } catch (error) {
69
+ console.error('AuthFetch error:', error);
70
+ throw error;
71
+ }
72
+ }, [oxyServices]);
73
+
74
+ // JSON convenience methods
75
+ const get = useCallback(async (endpoint: string, options?: AuthFetchOptions) => {
76
+ const response = await authFetch(endpoint, { ...options, method: 'GET' });
77
+ return handleJsonResponse(response);
78
+ }, [authFetch]);
79
+
80
+ const post = useCallback(async (endpoint: string, data?: any, options?: AuthFetchOptions) => {
81
+ const response = await authFetch(endpoint, {
82
+ ...options,
83
+ method: 'POST',
84
+ headers: {
85
+ 'Content-Type': 'application/json',
86
+ ...options?.headers
87
+ },
88
+ body: data ? JSON.stringify(data) : undefined
89
+ });
90
+ return handleJsonResponse(response);
91
+ }, [authFetch]);
92
+
93
+ const put = useCallback(async (endpoint: string, data?: any, options?: AuthFetchOptions) => {
94
+ const response = await authFetch(endpoint, {
95
+ ...options,
96
+ method: 'PUT',
97
+ headers: {
98
+ 'Content-Type': 'application/json',
99
+ ...options?.headers
100
+ },
101
+ body: data ? JSON.stringify(data) : undefined
102
+ });
103
+ return handleJsonResponse(response);
104
+ }, [authFetch]);
105
+
106
+ const del = useCallback(async (endpoint: string, options?: AuthFetchOptions) => {
107
+ const response = await authFetch(endpoint, { ...options, method: 'DELETE' });
108
+ return handleJsonResponse(response);
109
+ }, [authFetch]);
110
+
111
+ // Attach convenience methods and auth state to the main function
112
+ const fetchWithMethods = authFetch as AuthFetchAPI;
113
+ fetchWithMethods.get = get;
114
+ fetchWithMethods.post = post;
115
+ fetchWithMethods.put = put;
116
+ fetchWithMethods.delete = del;
117
+ fetchWithMethods.isAuthenticated = isAuthenticated;
118
+ fetchWithMethods.user = user;
119
+ fetchWithMethods.login = login;
120
+ fetchWithMethods.logout = logout;
121
+ fetchWithMethods.signUp = signUp;
122
+
123
+ return fetchWithMethods;
124
+ }
125
+
126
+ /**
127
+ * Helper functions
128
+ */
129
+
130
+ function resolveURL(input: RequestInfo | URL, baseURL: string): string {
131
+ const url = input.toString();
132
+
133
+ // If it's already a full URL, return as is
134
+ if (url.startsWith('http://') || url.startsWith('https://')) {
135
+ return url;
136
+ }
137
+
138
+ // If it starts with /, it's relative to base URL
139
+ if (url.startsWith('/')) {
140
+ return `${baseURL}${url}`;
141
+ }
142
+
143
+ // Otherwise, append to base URL with /
144
+ return `${baseURL}/${url}`;
145
+ }
146
+
147
+ async function addAuthHeaders(init?: AuthFetchOptions, oxyServices?: any): Promise<RequestInit> {
148
+ const headers = new Headers(init?.headers);
149
+
150
+ // Add auth header if user is authenticated
151
+ if (oxyServices?.getCurrentUserId() && !headers.has('Authorization')) {
152
+ // Try to get current access token
153
+ try {
154
+ const accessToken = oxyServices.getAccessToken?.() || oxyServices.accessToken;
155
+ if (accessToken) {
156
+ headers.set('Authorization', `Bearer ${accessToken}`);
157
+ }
158
+ } catch (error) {
159
+ // Ignore auth header errors
160
+ }
161
+ }
162
+
163
+ const body = init?.body;
164
+ const processedBody = body && typeof body === 'object' && !(body instanceof FormData) && !(body instanceof URLSearchParams)
165
+ ? JSON.stringify(body)
166
+ : body;
167
+
168
+ return {
169
+ ...init,
170
+ headers,
171
+ body: processedBody
172
+ };
173
+ }
174
+
175
+ async function handleJsonResponse(response: Response): Promise<any> {
176
+ if (!response.ok) {
177
+ let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
178
+
179
+ try {
180
+ const errorData = await response.json();
181
+ errorMessage = errorData.message || errorData.error || errorMessage;
182
+ } catch {
183
+ // Ignore JSON parsing errors
184
+ }
185
+
186
+ const error = new Error(errorMessage) as any;
187
+ error.status = response.status;
188
+ error.response = response;
189
+ throw error;
190
+ }
191
+
192
+ try {
193
+ return await response.json();
194
+ } catch {
195
+ // If response isn't JSON, return the response itself
196
+ return response;
197
+ }
198
+ }
199
+
200
+ export default useAuthFetch;
@@ -6,9 +6,9 @@ import { useOxy } from '../context/OxyContext';
6
6
 
7
7
  // Memoized selector to prevent unnecessary re-renders
8
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,
9
+ isFollowing: state.follow.followingUsers[userId] ?? false,
10
+ isLoading: state.follow.loadingUsers[userId] ?? false,
11
+ error: state.follow.errors[userId] ?? null,
12
12
  });
13
13
 
14
14
  // Memoized selector for multiple users
@@ -16,40 +16,20 @@ const createMultipleFollowSelector = (userIds: string[]) => (state: RootState) =
16
16
  const followData: Record<string, { isFollowing: boolean; isLoading: boolean; error: string | null }> = {};
17
17
  const followState = state.follow;
18
18
 
19
- // Defensive check for follow state
20
- if (!followState) {
21
- // Return default values if follow state is not initialized
22
- for (const userId of userIds) {
23
- followData[userId] = {
24
- isFollowing: false,
25
- isLoading: false,
26
- error: null,
27
- };
28
- }
29
-
30
- return {
31
- followData,
32
- isAnyLoading: false,
33
- hasAnyError: false,
34
- allFollowing: false,
35
- allNotFollowing: true,
36
- };
37
- }
38
-
39
19
  for (const userId of userIds) {
40
20
  followData[userId] = {
41
- isFollowing: followState.followingUsers?.[userId] ?? false,
42
- isLoading: followState.loadingUsers?.[userId] ?? false,
43
- error: followState.errors?.[userId] ?? null,
21
+ isFollowing: followState.followingUsers[userId] ?? false,
22
+ isLoading: followState.loadingUsers[userId] ?? false,
23
+ error: followState.errors[userId] ?? null,
44
24
  };
45
25
  }
46
26
 
47
27
  return {
48
28
  followData,
49
- isAnyLoading: userIds.some(uid => followState.loadingUsers?.[uid]),
50
- hasAnyError: userIds.some(uid => followState.errors?.[uid]),
51
- allFollowing: userIds.every(uid => followState.followingUsers?.[uid]),
52
- allNotFollowing: userIds.every(uid => !followState.followingUsers?.[uid]),
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]),
53
33
  };
54
34
  };
55
35
 
@@ -0,0 +1,188 @@
1
+ import { useDispatch, useSelector } from 'react-redux';
2
+ import { useCallback, useMemo } from 'react';
3
+ import {
4
+ toggleFollowUser,
5
+ setFollowingStatus,
6
+ clearFollowError,
7
+ fetchFollowStatus,
8
+ followSelectors
9
+ } from '../store/slices/followSlice';
10
+ import type { FollowState } from '../store/slices/types';
11
+ import { useOxy } from '../context/OxyContext';
12
+
13
+ // Generic type for state that includes follow slice
14
+ interface StateWithFollow {
15
+ follow: FollowState;
16
+ }
17
+
18
+ // Memoized selector to prevent unnecessary re-renders
19
+ const createFollowSelector = (userId: string) => (state: StateWithFollow) => ({
20
+ isFollowing: followSelectors.selectIsUserFollowed(state, userId),
21
+ isLoading: followSelectors.selectIsUserLoading(state, userId),
22
+ error: followSelectors.selectUserError(state, userId),
23
+ });
24
+
25
+ // Memoized selector for multiple users
26
+ const createMultipleFollowSelector = (userIds: string[]) => (state: StateWithFollow) => {
27
+ const followData: Record<string, { isFollowing: boolean; isLoading: boolean; error: string | null }> = {};
28
+
29
+ for (const userId of userIds) {
30
+ followData[userId] = {
31
+ isFollowing: followSelectors.selectIsUserFollowed(state, userId),
32
+ isLoading: followSelectors.selectIsUserLoading(state, userId),
33
+ error: followSelectors.selectUserError(state, userId),
34
+ };
35
+ }
36
+
37
+ const followState = state.follow;
38
+ return {
39
+ followData,
40
+ isAnyLoading: userIds.some(uid => followState.loadingUsers[uid]),
41
+ hasAnyError: userIds.some(uid => followState.errors[uid]),
42
+ allFollowing: userIds.every(uid => followState.followingUsers[uid]),
43
+ allNotFollowing: userIds.every(uid => !followState.followingUsers[uid]),
44
+ };
45
+ };
46
+
47
+ /**
48
+ * Custom hook for managing follow/unfollow functionality
49
+ * Works with any Redux store that includes the Oxy follow reducer
50
+ * Optimized to prevent unnecessary re-renders
51
+ * Can handle both single user and multiple users
52
+ */
53
+ export const useOxyFollow = (userId?: string | string[]) => {
54
+ const dispatch = useDispatch();
55
+ const { oxyServices } = useOxy();
56
+
57
+ // Memoize user IDs to prevent recreation on every render
58
+ const userIds = useMemo(() => {
59
+ return Array.isArray(userId) ? userId : userId ? [userId] : [];
60
+ }, [userId]);
61
+
62
+ const isSingleUser = typeof userId === 'string';
63
+
64
+ // Memoize selectors to prevent recreation
65
+ const singleUserSelector = useMemo(() => {
66
+ return isSingleUser && userId ? createFollowSelector(userId) : null;
67
+ }, [isSingleUser, userId]);
68
+
69
+ const multipleUserSelector = useMemo(() => {
70
+ return !isSingleUser ? createMultipleFollowSelector(userIds) : null;
71
+ }, [isSingleUser, userIds]);
72
+
73
+ // Use appropriate selector based on mode
74
+ const singleUserData = useSelector(singleUserSelector || (() => ({ isFollowing: false, isLoading: false, error: null })));
75
+ const multipleUserData = useSelector(multipleUserSelector || (() => ({
76
+ followData: {},
77
+ isAnyLoading: false,
78
+ hasAnyError: false,
79
+ allFollowing: false,
80
+ allNotFollowing: true
81
+ })));
82
+
83
+ // Memoized callbacks to prevent recreation on every render
84
+ const toggleFollow = useCallback(async () => {
85
+ if (!isSingleUser || !userId) throw new Error('toggleFollow is only available for single user mode');
86
+
87
+ try {
88
+ const result = await dispatch(toggleFollowUser({
89
+ userId,
90
+ oxyServices,
91
+ isCurrentlyFollowing: singleUserData.isFollowing
92
+ })).unwrap();
93
+ return result;
94
+ } catch (error) {
95
+ throw error;
96
+ }
97
+ }, [dispatch, userId, oxyServices, singleUserData.isFollowing, isSingleUser]);
98
+
99
+ const setFollowStatus = useCallback((following: boolean) => {
100
+ if (!isSingleUser || !userId) throw new Error('setFollowStatus is only available for single user mode');
101
+ dispatch(setFollowingStatus({ userId, isFollowing: following }));
102
+ }, [dispatch, userId, isSingleUser]);
103
+
104
+ const fetchStatus = useCallback(async () => {
105
+ if (!isSingleUser || !userId) throw new Error('fetchStatus is only available for single user mode');
106
+
107
+ try {
108
+ await dispatch(fetchFollowStatus({ userId, oxyServices })).unwrap();
109
+ } catch (error) {
110
+ console.warn(`Failed to fetch follow status for user ${userId}:`, error);
111
+ }
112
+ }, [dispatch, userId, oxyServices, isSingleUser]);
113
+
114
+ const clearError = useCallback(() => {
115
+ if (!isSingleUser || !userId) throw new Error('clearError is only available for single user mode');
116
+ dispatch(clearFollowError(userId));
117
+ }, [dispatch, userId, isSingleUser]);
118
+
119
+ // Multiple user callbacks
120
+ const toggleFollowForUser = useCallback(async (targetUserId: string) => {
121
+ const currentState = multipleUserData.followData[targetUserId]?.isFollowing ?? false;
122
+ try {
123
+ const result = await dispatch(toggleFollowUser({
124
+ userId: targetUserId,
125
+ oxyServices,
126
+ isCurrentlyFollowing: currentState
127
+ })).unwrap();
128
+ return result;
129
+ } catch (error) {
130
+ throw error;
131
+ }
132
+ }, [dispatch, oxyServices, multipleUserData.followData]);
133
+
134
+ const setFollowStatusForUser = useCallback((targetUserId: string, following: boolean) => {
135
+ dispatch(setFollowingStatus({ userId: targetUserId, isFollowing: following }));
136
+ }, [dispatch]);
137
+
138
+ const fetchStatusForUser = useCallback(async (targetUserId: string) => {
139
+ try {
140
+ await dispatch(fetchFollowStatus({ userId: targetUserId, oxyServices })).unwrap();
141
+ } catch (error) {
142
+ console.warn(`Failed to fetch follow status for user ${targetUserId}:`, error);
143
+ }
144
+ }, [dispatch, oxyServices]);
145
+
146
+ const fetchAllStatuses = useCallback(async () => {
147
+ const promises = userIds.map(uid =>
148
+ dispatch(fetchFollowStatus({ userId: uid, oxyServices })).unwrap().catch((error: any) => {
149
+ console.warn(`Failed to fetch follow status for user ${uid}:`, error);
150
+ })
151
+ );
152
+ await Promise.all(promises);
153
+ }, [dispatch, userIds, oxyServices]);
154
+
155
+ const clearErrorForUser = useCallback((targetUserId: string) => {
156
+ dispatch(clearFollowError(targetUserId));
157
+ }, [dispatch]);
158
+
159
+ // Return appropriate interface based on mode
160
+ if (isSingleUser && userId) {
161
+ return {
162
+ isFollowing: singleUserData.isFollowing,
163
+ isLoading: singleUserData.isLoading,
164
+ error: singleUserData.error,
165
+ toggleFollow,
166
+ setFollowStatus,
167
+ fetchStatus,
168
+ clearError,
169
+ };
170
+ }
171
+
172
+ return {
173
+ followData: multipleUserData.followData,
174
+ toggleFollowForUser,
175
+ setFollowStatusForUser,
176
+ fetchStatusForUser,
177
+ fetchAllStatuses,
178
+ clearErrorForUser,
179
+ // Helper methods
180
+ isAnyLoading: multipleUserData.isAnyLoading,
181
+ hasAnyError: multipleUserData.hasAnyError,
182
+ allFollowing: multipleUserData.allFollowing,
183
+ allNotFollowing: multipleUserData.allNotFollowing,
184
+ };
185
+ };
186
+
187
+ // Backward compatibility alias
188
+ export const useFollow = useOxyFollow;
package/src/ui/index.ts CHANGED
@@ -21,7 +21,38 @@ export {
21
21
  OxyContextProviderProps
22
22
  } from './context/OxyContext';
23
23
 
24
- // Redux store exports
24
+ // Redux store exports - NEW ARCHITECTURE
25
+ export {
26
+ setupOxyStore,
27
+ oxyReducers,
28
+ // Individual slices
29
+ authSlice,
30
+ authActions,
31
+ authSelectors,
32
+ authReducer,
33
+ followSlice,
34
+ followActions,
35
+ followSelectors,
36
+ followThunks,
37
+ followReducer,
38
+ // Action creators
39
+ loginStart,
40
+ loginSuccess,
41
+ loginFailure,
42
+ logout,
43
+ setFollowingStatus,
44
+ clearFollowError,
45
+ resetFollowState,
46
+ fetchFollowStatus,
47
+ toggleFollowUser,
48
+ // Types
49
+ AuthState,
50
+ FollowState,
51
+ initialAuthState,
52
+ initialFollowState
53
+ } from './store';
54
+
55
+ // Legacy store exports (deprecated)
25
56
  export { store } from './store';
26
57
  export type { RootState, AppDispatch } from './store';
27
58
 
@@ -32,7 +63,8 @@ export { fontFamilies, fontStyles } from './styles/fonts';
32
63
  export * from './navigation/types';
33
64
 
34
65
  // Hooks
35
- export { useFollow } from './hooks';
66
+ export { useOxyFollow, useFollow } from './hooks';
67
+ export { default as useAuthFetch } from './hooks/useAuthFetch';
36
68
 
37
69
  // Screens
38
70
  export { default as ProfileScreen } from './screens/ProfileScreen';
@@ -120,15 +120,35 @@ export interface OxyProviderProps {
120
120
  onAuthStateChange?: (user: User | null) => void;
121
121
 
122
122
  /**
123
- * Prefix for keys in AsyncStorage
124
- * @default "oxy"
123
+ * Storage key prefix for AsyncStorage
125
124
  */
126
125
  storageKeyPrefix?: string;
127
126
 
128
127
  /**
129
- * Whether to show the internal toaster in the bottom sheet
130
- * If false, only the provider's global toaster will be shown
128
+ * Whether to show the internal toaster
131
129
  * @default true
132
130
  */
133
131
  showInternalToaster?: boolean;
132
+
133
+ /**
134
+ * External Redux store to use instead of the internal store
135
+ * If provided, the store must include the Oxy reducers using setupOxyStore()
136
+ * @example
137
+ * ```ts
138
+ * const store = configureStore({
139
+ * reducer: {
140
+ * ...setupOxyStore(),
141
+ * myAppReducer,
142
+ * },
143
+ * });
144
+ * ```
145
+ */
146
+ store?: any;
147
+
148
+ /**
149
+ * Skip Redux Provider wrapper if store is managed externally
150
+ * Set to true if your app already has a Redux Provider higher in the component tree
151
+ * @default false
152
+ */
153
+ skipReduxProvider?: boolean;
134
154
  }
@@ -15,13 +15,11 @@ 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
24
- } from '../components';
18
+ import ProfileCard from '../components/ProfileCard';
19
+ import Section from '../components/Section';
20
+ import QuickActions from '../components/QuickActions';
21
+ import GroupedSection from '../components/GroupedSection';
22
+ import GroupedItem from '../components/GroupedItem';
25
23
 
26
24
  const AccountCenterScreen: React.FC<BaseScreenProps> = ({
27
25
  onClose,
@@ -69,15 +69,18 @@ const AppInfoScreen: React.FC<BaseScreenProps> = ({
69
69
  // Check API connection on mount
70
70
  const checkConnection = async () => {
71
71
  setConnectionStatus('checking');
72
-
73
- if (!oxyServices) {
74
- setConnectionStatus('disconnected');
75
- return;
76
- }
77
-
72
+ const apiBaseUrl = oxyServices?.getBaseURL() || 'https://api.oxy.so';
78
73
  try {
79
- await oxyServices.healthCheck();
80
- setConnectionStatus('connected');
74
+ const response = await fetch(`${apiBaseUrl}/`, {
75
+ method: 'GET',
76
+ timeout: 3000,
77
+ } as any);
78
+
79
+ if (response.ok) {
80
+ setConnectionStatus('connected');
81
+ } else {
82
+ setConnectionStatus('disconnected');
83
+ }
81
84
  } catch (error) {
82
85
  setConnectionStatus('disconnected');
83
86
  }
@@ -118,11 +121,22 @@ const AppInfoScreen: React.FC<BaseScreenProps> = ({
118
121
  toast.info('Running system checks...', { duration: 2000 });
119
122
 
120
123
  try {
121
- const data = await oxyServices.healthCheck();
122
- checks.push('✅ API server is responding');
123
- checks.push(`📊 Server stats: ${data.users || 0} users`);
124
- checks.push(`🌐 API URL: ${apiBaseUrl}`);
125
- setConnectionStatus('connected');
124
+ const response = await fetch(`${apiBaseUrl}/`, {
125
+ method: 'GET',
126
+ timeout: 5000,
127
+ } as any);
128
+
129
+ if (response.ok) {
130
+ const data = await response.json();
131
+ checks.push('✅ API server is responding');
132
+ checks.push(`📊 Server stats: ${data.users || 0} users`);
133
+ checks.push(`🌐 API URL: ${apiBaseUrl}`);
134
+ setConnectionStatus('connected');
135
+ } else {
136
+ checks.push('❌ API server returned error status');
137
+ checks.push(` Status: ${response.status} ${response.statusText}`);
138
+ setConnectionStatus('disconnected');
139
+ }
126
140
  } catch (error) {
127
141
  checks.push('❌ API server connection failed');
128
142
  checks.push(` Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
@@ -500,17 +514,20 @@ const AppInfoScreen: React.FC<BaseScreenProps> = ({
500
514
  }
501
515
  onPress={async () => {
502
516
  setConnectionStatus('checking');
503
-
504
- if (!oxyServices) {
505
- setConnectionStatus('disconnected');
506
- toast.error('OxyServices not initialized');
507
- return;
508
- }
509
-
517
+ const apiBaseUrl = oxyServices?.getBaseURL() || 'https://api.oxy.so';
510
518
  try {
511
- await oxyServices.healthCheck();
512
- setConnectionStatus('connected');
513
- toast.success('API connection successful');
519
+ const response = await fetch(`${apiBaseUrl}/`, {
520
+ method: 'GET',
521
+ timeout: 3000,
522
+ } as any);
523
+
524
+ if (response.ok) {
525
+ setConnectionStatus('connected');
526
+ toast.success('API connection successful');
527
+ } else {
528
+ setConnectionStatus('disconnected');
529
+ toast.error(`API server error: ${response.status}`);
530
+ }
514
531
  } catch (error) {
515
532
  setConnectionStatus('disconnected');
516
533
  toast.error('Failed to connect to API server');