@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.
- package/lib/commonjs/core/index.js +0 -59
- package/lib/commonjs/core/index.js.map +1 -1
- package/lib/commonjs/index.js +174 -17
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/FollowButton.js +8 -23
- package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
- package/lib/commonjs/ui/components/OxyProvider.js +49 -38
- package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
- package/lib/commonjs/ui/components/OxySignInButton.js +2 -8
- package/lib/commonjs/ui/components/OxySignInButton.js.map +1 -1
- package/lib/commonjs/ui/hooks/index.js +15 -2
- package/lib/commonjs/ui/hooks/index.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAuthFetch.js +182 -0
- package/lib/commonjs/ui/hooks/useAuthFetch.js.map +1 -0
- package/lib/commonjs/ui/hooks/useFollow.js +10 -29
- package/lib/commonjs/ui/hooks/useFollow.js.map +1 -1
- package/lib/commonjs/ui/hooks/useOxyFollow.js +190 -0
- package/lib/commonjs/ui/hooks/useOxyFollow.js.map +1 -0
- package/lib/commonjs/ui/index.js +183 -0
- package/lib/commonjs/ui/index.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountCenterScreen.js +18 -14
- package/lib/commonjs/ui/screens/AccountCenterScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AppInfoScreen.js +37 -19
- package/lib/commonjs/ui/screens/AppInfoScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/FileManagementScreen.js +27 -9
- package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/karma/KarmaRewardsScreen.js +2 -8
- package/lib/commonjs/ui/screens/karma/KarmaRewardsScreen.js.map +1 -1
- package/lib/commonjs/ui/store/index.js +51 -255
- package/lib/commonjs/ui/store/index.js.map +1 -1
- package/lib/commonjs/ui/store/setupOxyStore.js +63 -0
- package/lib/commonjs/ui/store/setupOxyStore.js.map +1 -0
- package/lib/commonjs/ui/store/slices/authSlice.js +56 -0
- package/lib/commonjs/ui/store/slices/authSlice.js.map +1 -0
- package/lib/commonjs/ui/store/slices/followSlice.js +238 -0
- package/lib/commonjs/ui/store/slices/followSlice.js.map +1 -0
- package/lib/commonjs/ui/store/slices/index.js +129 -0
- package/lib/commonjs/ui/store/slices/index.js.map +1 -0
- package/lib/commonjs/ui/store/slices/types.js +19 -0
- package/lib/commonjs/ui/store/slices/types.js.map +1 -0
- package/lib/commonjs/ui/styles/index.js +11 -0
- package/lib/commonjs/ui/styles/index.js.map +1 -1
- package/lib/commonjs/ui/styles/shadows.js +123 -0
- package/lib/commonjs/ui/styles/shadows.js.map +1 -0
- package/lib/module/core/index.js +0 -59
- package/lib/module/core/index.js.map +1 -1
- package/lib/module/index.js +14 -10
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/FollowButton.js +8 -23
- package/lib/module/ui/components/FollowButton.js.map +1 -1
- package/lib/module/ui/components/OxyProvider.js +49 -38
- package/lib/module/ui/components/OxyProvider.js.map +1 -1
- package/lib/module/ui/components/OxySignInButton.js +2 -8
- package/lib/module/ui/components/OxySignInButton.js.map +1 -1
- package/lib/module/ui/hooks/index.js +2 -1
- package/lib/module/ui/hooks/index.js.map +1 -1
- package/lib/module/ui/hooks/useAuthFetch.js +177 -0
- package/lib/module/ui/hooks/useAuthFetch.js.map +1 -0
- package/lib/module/ui/hooks/useFollow.js +10 -29
- package/lib/module/ui/hooks/useFollow.js.map +1 -1
- package/lib/module/ui/hooks/useOxyFollow.js +186 -0
- package/lib/module/ui/hooks/useOxyFollow.js.map +1 -0
- package/lib/module/ui/index.js +12 -2
- package/lib/module/ui/index.js.map +1 -1
- package/lib/module/ui/screens/AccountCenterScreen.js +5 -1
- package/lib/module/ui/screens/AccountCenterScreen.js.map +1 -1
- package/lib/module/ui/screens/AppInfoScreen.js +37 -19
- package/lib/module/ui/screens/AppInfoScreen.js.map +1 -1
- package/lib/module/ui/screens/FileManagementScreen.js +27 -9
- package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/module/ui/screens/karma/KarmaRewardsScreen.js +2 -8
- package/lib/module/ui/screens/karma/KarmaRewardsScreen.js.map +1 -1
- package/lib/module/ui/store/index.js +23 -249
- package/lib/module/ui/store/index.js.map +1 -1
- package/lib/module/ui/store/setupOxyStore.js +59 -0
- package/lib/module/ui/store/setupOxyStore.js.map +1 -0
- package/lib/module/ui/store/slices/authSlice.js +48 -0
- package/lib/module/ui/store/slices/authSlice.js.map +1 -0
- package/lib/module/ui/store/slices/followSlice.js +232 -0
- package/lib/module/ui/store/slices/followSlice.js.map +1 -0
- package/lib/module/ui/store/slices/index.js +11 -0
- package/lib/module/ui/store/slices/index.js.map +1 -0
- package/lib/module/ui/store/slices/types.js +15 -0
- package/lib/module/ui/store/slices/types.js.map +1 -0
- package/lib/module/ui/styles/index.js +1 -0
- package/lib/module/ui/styles/index.js.map +1 -1
- package/lib/module/ui/styles/shadows.js +119 -0
- package/lib/module/ui/styles/shadows.js.map +1 -0
- package/lib/typescript/core/index.d.ts +0 -28
- package/lib/typescript/core/index.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +3 -5
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/ui/components/FollowButton.d.ts.map +1 -1
- package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
- package/lib/typescript/ui/components/OxySignInButton.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/index.d.ts +2 -1
- package/lib/typescript/ui/hooks/index.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/useAuthFetch.d.ts +33 -0
- package/lib/typescript/ui/hooks/useAuthFetch.d.ts.map +1 -0
- package/lib/typescript/ui/hooks/useFollow.d.ts.map +1 -1
- package/lib/typescript/ui/hooks/useOxyFollow.d.ts +81 -0
- package/lib/typescript/ui/hooks/useOxyFollow.d.ts.map +1 -0
- package/lib/typescript/ui/index.d.ts +3 -1
- package/lib/typescript/ui/index.d.ts.map +1 -1
- package/lib/typescript/ui/navigation/types.d.ts +22 -4
- package/lib/typescript/ui/navigation/types.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AccountCenterScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/AppInfoScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/ui/screens/karma/KarmaRewardsScreen.d.ts.map +1 -1
- package/lib/typescript/ui/store/index.d.ts +19 -58
- package/lib/typescript/ui/store/index.d.ts.map +1 -1
- package/lib/typescript/ui/store/setupOxyStore.d.ts +29 -0
- package/lib/typescript/ui/store/setupOxyStore.d.ts.map +1 -0
- package/lib/typescript/ui/store/slices/authSlice.d.ts +32 -0
- package/lib/typescript/ui/store/slices/authSlice.d.ts.map +1 -0
- package/lib/typescript/ui/store/slices/followSlice.d.ts +120 -0
- package/lib/typescript/ui/store/slices/followSlice.d.ts.map +1 -0
- package/lib/typescript/ui/store/slices/index.d.ts +9 -0
- package/lib/typescript/ui/store/slices/index.d.ts.map +1 -0
- package/lib/typescript/ui/store/slices/types.d.ts +16 -0
- package/lib/typescript/ui/store/slices/types.d.ts.map +1 -0
- package/lib/typescript/ui/styles/index.d.ts +1 -0
- package/lib/typescript/ui/styles/index.d.ts.map +1 -1
- package/lib/typescript/ui/styles/shadows.d.ts +233 -0
- package/lib/typescript/ui/styles/shadows.d.ts.map +1 -0
- package/package.json +14 -15
- package/src/__tests__/ui/hooks/useOxyFollow.test.tsx +92 -0
- package/src/__tests__/ui/store/setupOxyStore.test.ts +50 -0
- package/src/__tests__/validate-structure.js +91 -0
- package/src/__tests__/validation.js +42 -0
- package/src/core/index.ts +0 -66
- package/src/index.ts +36 -4
- package/src/ui/components/FollowButton.tsx +11 -25
- package/src/ui/components/OxyProvider.tsx +48 -33
- package/src/ui/components/OxySignInButton.tsx +2 -6
- package/src/ui/hooks/index.ts +2 -1
- package/src/ui/hooks/useAuthFetch.ts +200 -0
- package/src/ui/hooks/useFollow.ts +10 -30
- package/src/ui/hooks/useOxyFollow.ts +188 -0
- package/src/ui/index.ts +34 -2
- package/src/ui/navigation/types.ts +24 -4
- package/src/ui/screens/AccountCenterScreen.tsx +5 -7
- package/src/ui/screens/AppInfoScreen.tsx +40 -23
- package/src/ui/screens/FileManagementScreen.tsx +268 -248
- package/src/ui/screens/karma/KarmaRewardsScreen.tsx +2 -5
- package/src/ui/store/index.ts +31 -245
- package/src/ui/store/setupOxyStore.ts +58 -0
- package/src/ui/store/slices/authSlice.ts +43 -0
- package/src/ui/store/slices/followSlice.ts +207 -0
- package/src/ui/store/slices/index.ts +31 -0
- package/src/ui/store/slices/types.ts +33 -0
- package/src/ui/styles/index.ts +1 -0
- 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
|
|
10
|
-
isLoading: state.follow
|
|
11
|
-
error: state.follow
|
|
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
|
|
42
|
-
isLoading: followState.loadingUsers
|
|
43
|
-
error: followState.errors
|
|
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
|
|
50
|
-
hasAnyError: userIds.some(uid => followState.errors
|
|
51
|
-
allFollowing: userIds.every(uid => followState.followingUsers
|
|
52
|
-
allNotFollowing: userIds.every(uid => !followState.followingUsers
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
80
|
-
|
|
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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
512
|
-
|
|
513
|
-
|
|
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');
|