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