@oxyhq/services 5.8.1 → 5.8.2

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 (137) hide show
  1. package/lib/commonjs/index.js +9 -27
  2. package/lib/commonjs/index.js.map +1 -1
  3. package/lib/commonjs/node/createAuth.js +7 -585
  4. package/lib/commonjs/node/createAuth.js.map +1 -1
  5. package/lib/commonjs/node/index.js +1 -38
  6. package/lib/commonjs/node/index.js.map +1 -1
  7. package/lib/commonjs/ui/components/FollowButton.js +100 -12
  8. package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
  9. package/lib/commonjs/ui/components/Header.js +40 -6
  10. package/lib/commonjs/ui/components/Header.js.map +1 -1
  11. package/lib/commonjs/ui/components/OxyProvider.js +5 -0
  12. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  13. package/lib/commonjs/ui/context/OxyContext.js +63 -125
  14. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  15. package/lib/commonjs/ui/hooks/index.js +6 -0
  16. package/lib/commonjs/ui/hooks/index.js.map +1 -1
  17. package/lib/commonjs/ui/hooks/useFollow.js +59 -2
  18. package/lib/commonjs/ui/hooks/useFollow.js.map +1 -1
  19. package/lib/commonjs/ui/navigation/OxyRouter.js +10 -0
  20. package/lib/commonjs/ui/navigation/OxyRouter.js.map +1 -1
  21. package/lib/commonjs/ui/screens/AccountSettingsScreen.js +9 -0
  22. package/lib/commonjs/ui/screens/AccountSettingsScreen.js.map +1 -1
  23. package/lib/commonjs/ui/screens/ProfileScreen.js +214 -37
  24. package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -1
  25. package/lib/commonjs/ui/screens/UserLinksScreen.js +90 -0
  26. package/lib/commonjs/ui/screens/UserLinksScreen.js.map +1 -0
  27. package/lib/commonjs/ui/screens/karma/KarmaAboutScreen.js +9 -6
  28. package/lib/commonjs/ui/screens/karma/KarmaAboutScreen.js.map +1 -1
  29. package/lib/commonjs/ui/screens/karma/KarmaCenterScreen.js +3 -30
  30. package/lib/commonjs/ui/screens/karma/KarmaCenterScreen.js.map +1 -1
  31. package/lib/commonjs/ui/screens/karma/KarmaFAQScreen.js +37 -46
  32. package/lib/commonjs/ui/screens/karma/KarmaFAQScreen.js.map +1 -1
  33. package/lib/commonjs/ui/screens/karma/KarmaLeaderboardScreen.js +9 -12
  34. package/lib/commonjs/ui/screens/karma/KarmaLeaderboardScreen.js.map +1 -1
  35. package/lib/commonjs/ui/screens/karma/KarmaRewardsScreen.js +9 -12
  36. package/lib/commonjs/ui/screens/karma/KarmaRewardsScreen.js.map +1 -1
  37. package/lib/commonjs/ui/screens/karma/KarmaRulesScreen.js +9 -12
  38. package/lib/commonjs/ui/screens/karma/KarmaRulesScreen.js.map +1 -1
  39. package/lib/commonjs/ui/stores/authStore.js +24 -6
  40. package/lib/commonjs/ui/stores/authStore.js.map +1 -1
  41. package/lib/commonjs/ui/stores/followStore.js +106 -1
  42. package/lib/commonjs/ui/stores/followStore.js.map +1 -1
  43. package/lib/module/index.js +1 -3
  44. package/lib/module/index.js.map +1 -1
  45. package/lib/module/node/createAuth.js +7 -584
  46. package/lib/module/node/createAuth.js.map +1 -1
  47. package/lib/module/node/index.js +1 -7
  48. package/lib/module/node/index.js.map +1 -1
  49. package/lib/module/ui/components/FollowButton.js +101 -13
  50. package/lib/module/ui/components/FollowButton.js.map +1 -1
  51. package/lib/module/ui/components/Header.js +40 -6
  52. package/lib/module/ui/components/Header.js.map +1 -1
  53. package/lib/module/ui/components/OxyProvider.js +5 -0
  54. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  55. package/lib/module/ui/context/OxyContext.js +63 -125
  56. package/lib/module/ui/context/OxyContext.js.map +1 -1
  57. package/lib/module/ui/hooks/index.js +1 -1
  58. package/lib/module/ui/hooks/index.js.map +1 -1
  59. package/lib/module/ui/hooks/useFollow.js +57 -1
  60. package/lib/module/ui/hooks/useFollow.js.map +1 -1
  61. package/lib/module/ui/navigation/OxyRouter.js +10 -0
  62. package/lib/module/ui/navigation/OxyRouter.js.map +1 -1
  63. package/lib/module/ui/screens/AccountSettingsScreen.js +9 -0
  64. package/lib/module/ui/screens/AccountSettingsScreen.js.map +1 -1
  65. package/lib/module/ui/screens/ProfileScreen.js +214 -37
  66. package/lib/module/ui/screens/ProfileScreen.js.map +1 -1
  67. package/lib/module/ui/screens/UserLinksScreen.js +85 -0
  68. package/lib/module/ui/screens/UserLinksScreen.js.map +1 -0
  69. package/lib/module/ui/screens/karma/KarmaAboutScreen.js +9 -6
  70. package/lib/module/ui/screens/karma/KarmaAboutScreen.js.map +1 -1
  71. package/lib/module/ui/screens/karma/KarmaCenterScreen.js +3 -30
  72. package/lib/module/ui/screens/karma/KarmaCenterScreen.js.map +1 -1
  73. package/lib/module/ui/screens/karma/KarmaFAQScreen.js +37 -46
  74. package/lib/module/ui/screens/karma/KarmaFAQScreen.js.map +1 -1
  75. package/lib/module/ui/screens/karma/KarmaLeaderboardScreen.js +9 -12
  76. package/lib/module/ui/screens/karma/KarmaLeaderboardScreen.js.map +1 -1
  77. package/lib/module/ui/screens/karma/KarmaRewardsScreen.js +9 -12
  78. package/lib/module/ui/screens/karma/KarmaRewardsScreen.js.map +1 -1
  79. package/lib/module/ui/screens/karma/KarmaRulesScreen.js +9 -12
  80. package/lib/module/ui/screens/karma/KarmaRulesScreen.js.map +1 -1
  81. package/lib/module/ui/stores/authStore.js +24 -6
  82. package/lib/module/ui/stores/authStore.js.map +1 -1
  83. package/lib/module/ui/stores/followStore.js +106 -1
  84. package/lib/module/ui/stores/followStore.js.map +1 -1
  85. package/lib/typescript/index.d.ts +1 -1
  86. package/lib/typescript/index.d.ts.map +1 -1
  87. package/lib/typescript/node/createAuth.d.ts +0 -112
  88. package/lib/typescript/node/createAuth.d.ts.map +1 -1
  89. package/lib/typescript/node/index.d.ts +0 -2
  90. package/lib/typescript/node/index.d.ts.map +1 -1
  91. package/lib/typescript/ui/components/FollowButton.d.ts +1 -0
  92. package/lib/typescript/ui/components/FollowButton.d.ts.map +1 -1
  93. package/lib/typescript/ui/components/Header.d.ts +2 -0
  94. package/lib/typescript/ui/components/Header.d.ts.map +1 -1
  95. package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
  96. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  97. package/lib/typescript/ui/hooks/index.d.ts +1 -1
  98. package/lib/typescript/ui/hooks/index.d.ts.map +1 -1
  99. package/lib/typescript/ui/hooks/useFollow.d.ts +20 -0
  100. package/lib/typescript/ui/hooks/useFollow.d.ts.map +1 -1
  101. package/lib/typescript/ui/navigation/OxyRouter.d.ts.map +1 -1
  102. package/lib/typescript/ui/screens/AccountSettingsScreen.d.ts.map +1 -1
  103. package/lib/typescript/ui/screens/ProfileScreen.d.ts.map +1 -1
  104. package/lib/typescript/ui/screens/UserLinksScreen.d.ts +15 -0
  105. package/lib/typescript/ui/screens/UserLinksScreen.d.ts.map +1 -0
  106. package/lib/typescript/ui/screens/karma/KarmaAboutScreen.d.ts.map +1 -1
  107. package/lib/typescript/ui/screens/karma/KarmaCenterScreen.d.ts.map +1 -1
  108. package/lib/typescript/ui/screens/karma/KarmaFAQScreen.d.ts.map +1 -1
  109. package/lib/typescript/ui/screens/karma/KarmaLeaderboardScreen.d.ts.map +1 -1
  110. package/lib/typescript/ui/screens/karma/KarmaRewardsScreen.d.ts.map +1 -1
  111. package/lib/typescript/ui/screens/karma/KarmaRulesScreen.d.ts.map +1 -1
  112. package/lib/typescript/ui/stores/authStore.d.ts +3 -1
  113. package/lib/typescript/ui/stores/authStore.d.ts.map +1 -1
  114. package/lib/typescript/ui/stores/followStore.d.ts +10 -0
  115. package/lib/typescript/ui/stores/followStore.d.ts.map +1 -1
  116. package/package.json +1 -1
  117. package/src/index.ts +2 -10
  118. package/src/node/createAuth.ts +7 -623
  119. package/src/node/index.ts +1 -19
  120. package/src/ui/components/FollowButton.tsx +95 -11
  121. package/src/ui/components/Header.tsx +45 -4
  122. package/src/ui/components/OxyProvider.tsx +6 -0
  123. package/src/ui/context/OxyContext.tsx +65 -136
  124. package/src/ui/hooks/index.ts +1 -1
  125. package/src/ui/hooks/useFollow.ts +63 -0
  126. package/src/ui/navigation/OxyRouter.tsx +10 -0
  127. package/src/ui/screens/AccountSettingsScreen.tsx +8 -0
  128. package/src/ui/screens/ProfileScreen.tsx +191 -28
  129. package/src/ui/screens/UserLinksScreen.tsx +96 -0
  130. package/src/ui/screens/karma/KarmaAboutScreen.tsx +9 -2
  131. package/src/ui/screens/karma/KarmaCenterScreen.tsx +1 -20
  132. package/src/ui/screens/karma/KarmaFAQScreen.tsx +40 -24
  133. package/src/ui/screens/karma/KarmaLeaderboardScreen.tsx +9 -3
  134. package/src/ui/screens/karma/KarmaRewardsScreen.tsx +9 -3
  135. package/src/ui/screens/karma/KarmaRulesScreen.tsx +9 -3
  136. package/src/ui/stores/authStore.ts +22 -7
  137. package/src/ui/stores/followStore.ts +102 -1
@@ -14,6 +14,11 @@ export const useFollow = (userId?: string | string[]) => {
14
14
  const isFollowing = isSingleUser && userId ? followState.followingUsers[userId] ?? false : false;
15
15
  const isLoading = isSingleUser && userId ? followState.loadingUsers[userId] ?? false : false;
16
16
  const error = isSingleUser && userId ? followState.errors[userId] ?? null : null;
17
+
18
+ // Follower count helpers
19
+ const followerCount = isSingleUser && userId ? followState.followerCounts[userId] ?? null : null;
20
+ const followingCount = isSingleUser && userId ? followState.followingCounts[userId] ?? null : null;
21
+ const isLoadingCounts = isSingleUser && userId ? followState.loadingCounts[userId] ?? false : false;
17
22
 
18
23
  const toggleFollow = useCallback(async () => {
19
24
  if (!isSingleUser || !userId) throw new Error('toggleFollow is only available for single user mode');
@@ -35,6 +40,21 @@ export const useFollow = (userId?: string | string[]) => {
35
40
  followState.clearFollowError(userId);
36
41
  }, [isSingleUser, userId, followState]);
37
42
 
43
+ const fetchUserCounts = useCallback(async () => {
44
+ if (!isSingleUser || !userId) throw new Error('fetchUserCounts is only available for single user mode');
45
+ await followState.fetchUserCounts(userId, oxyServices);
46
+ }, [isSingleUser, userId, followState, oxyServices]);
47
+
48
+ const setFollowerCount = useCallback((count: number) => {
49
+ if (!isSingleUser || !userId) throw new Error('setFollowerCount is only available for single user mode');
50
+ followState.setFollowerCount(userId, count);
51
+ }, [isSingleUser, userId, followState]);
52
+
53
+ const setFollowingCount = useCallback((count: number) => {
54
+ if (!isSingleUser || !userId) throw new Error('setFollowingCount is only available for single user mode');
55
+ followState.setFollowingCount(userId, count);
56
+ }, [isSingleUser, userId, followState]);
57
+
38
58
  // Multiple user helpers
39
59
  const followData = useMemo(() => {
40
60
  const data: Record<string, { isFollowing: boolean; isLoading: boolean; error: string | null }> = {};
@@ -69,6 +89,11 @@ export const useFollow = (userId?: string | string[]) => {
69
89
  followState.clearFollowError(targetUserId);
70
90
  }, [followState]);
71
91
 
92
+ const updateCountsFromFollowAction = useCallback((targetUserId: string, action: 'follow' | 'unfollow', counts: { followers: number; following: number }) => {
93
+ const currentUserId = oxyServices.getCurrentUserId() || undefined;
94
+ followState.updateCountsFromFollowAction(targetUserId, action, counts, currentUserId);
95
+ }, [followState, oxyServices]);
96
+
72
97
  // Aggregate helpers for multiple users
73
98
  const isAnyLoading = userIds.some(uid => followState.loadingUsers[uid]);
74
99
  const hasAnyError = userIds.some(uid => !!followState.errors[uid]);
@@ -84,6 +109,13 @@ export const useFollow = (userId?: string | string[]) => {
84
109
  setFollowStatus,
85
110
  fetchStatus,
86
111
  clearError,
112
+ // Follower count methods
113
+ followerCount,
114
+ followingCount,
115
+ isLoadingCounts,
116
+ fetchUserCounts,
117
+ setFollowerCount,
118
+ setFollowingCount,
87
119
  };
88
120
  }
89
121
 
@@ -99,4 +131,35 @@ export const useFollow = (userId?: string | string[]) => {
99
131
  allFollowing,
100
132
  allNotFollowing,
101
133
  };
134
+ };
135
+
136
+ // Convenience hook for just follower counts
137
+ export const useFollowerCounts = (userId: string) => {
138
+ const { oxyServices } = useOxy();
139
+ const followState = useFollowStore();
140
+
141
+ const followerCount = followState.followerCounts[userId] ?? null;
142
+ const followingCount = followState.followingCounts[userId] ?? null;
143
+ const isLoadingCounts = followState.loadingCounts[userId] ?? false;
144
+
145
+ const fetchUserCounts = useCallback(async () => {
146
+ await followState.fetchUserCounts(userId, oxyServices);
147
+ }, [userId, followState, oxyServices]);
148
+
149
+ const setFollowerCount = useCallback((count: number) => {
150
+ followState.setFollowerCount(userId, count);
151
+ }, [userId, followState]);
152
+
153
+ const setFollowingCount = useCallback((count: number) => {
154
+ followState.setFollowingCount(userId, count);
155
+ }, [userId, followState]);
156
+
157
+ return {
158
+ followerCount,
159
+ followingCount,
160
+ isLoadingCounts,
161
+ fetchUserCounts,
162
+ setFollowerCount,
163
+ setFollowingCount,
164
+ };
102
165
  };
@@ -20,6 +20,7 @@ import KarmaAboutScreen from '../screens/karma/KarmaAboutScreen';
20
20
  import KarmaRewardsScreen from '../screens/karma/KarmaRewardsScreen';
21
21
  import KarmaFAQScreen from '../screens/karma/KarmaFAQScreen';
22
22
  import ProfileScreen from '../screens/ProfileScreen';
23
+ import UserLinksScreen from '../screens/UserLinksScreen';
23
24
  import FileManagementScreen from '../screens/FileManagementScreen';
24
25
  import RecoverAccountScreen from '../screens/RecoverAccountScreen';
25
26
  import PaymentGatewayScreen from '../screens/PaymentGatewayScreen';
@@ -101,6 +102,10 @@ const routes: Record<string, RouteConfig> = {
101
102
  component: ProfileScreen,
102
103
  snapPoints: ['60%', '90%'],
103
104
  },
105
+ UserLinks: {
106
+ component: UserLinksScreen,
107
+ snapPoints: ['60%', '90%'],
108
+ },
104
109
  FileManagement: {
105
110
  component: FileManagementScreen,
106
111
  snapPoints: ['70%', '100%'],
@@ -135,11 +140,14 @@ const OxyRouter: React.FC<OxyRouterProps> = ({
135
140
 
136
141
  // Memoized navigation methods
137
142
  const navigate = useCallback((screen: string, props: Record<string, any> = {}) => {
143
+ console.log('OxyRouter: navigate called with screen:', screen, 'props:', props);
138
144
  if (routes[screen]) {
145
+ console.log('OxyRouter: screen found in routes, navigating to:', screen);
139
146
  setCurrentScreen(screen);
140
147
  setScreenHistory(prev => [...prev, screen]);
141
148
  setScreenPropsMap(prev => ({ ...prev, [screen]: props }));
142
149
  } else {
150
+ console.error(`OxyRouter: Screen "${screen}" not found in routes:`, Object.keys(routes));
143
151
  if (process.env.NODE_ENV !== 'production') {
144
152
  console.error(`Screen "${screen}" not found`);
145
153
  }
@@ -165,10 +173,12 @@ const OxyRouter: React.FC<OxyRouterProps> = ({
165
173
  useEffect(() => {
166
174
  if (navigationRef) {
167
175
  navigationRef.current = navigate;
176
+ console.log('OxyRouter: navigationRef.current set to navigate function');
168
177
  }
169
178
  return () => {
170
179
  if (navigationRef) {
171
180
  navigationRef.current = null;
181
+ console.log('OxyRouter: navigationRef.current cleared');
172
182
  }
173
183
  };
174
184
  }, [navigate, navigationRef]);
@@ -1019,6 +1019,14 @@ const AccountSettingsScreen: React.FC<BaseScreenProps> = ({
1019
1019
 
1020
1020
  <GroupedSection
1021
1021
  items={[
1022
+ {
1023
+ id: 'preview-profile',
1024
+ icon: 'eye',
1025
+ iconColor: '#007AFF',
1026
+ title: 'Preview Profile',
1027
+ subtitle: 'See how your profile looks to others',
1028
+ onPress: () => navigate?.('Profile', { userId: user?.id }),
1029
+ },
1022
1030
  {
1023
1031
  id: 'privacy-settings',
1024
1032
  icon: 'shield-checkmark',
@@ -1,8 +1,10 @@
1
1
  import React, { useEffect, useState } from 'react';
2
- import { View, Text, StyleSheet, ActivityIndicator, ScrollView, TouchableOpacity } from 'react-native';
2
+ import { View, Text, StyleSheet, ActivityIndicator, ScrollView, TouchableOpacity, Image } from 'react-native';
3
3
  import { BaseScreenProps } from '../navigation/types';
4
4
  import { useOxy } from '../context/OxyContext';
5
5
  import Avatar from '../components/Avatar';
6
+ import { FollowButton } from '../components';
7
+ import { useFollow } from '../hooks/useFollow';
6
8
  import { Ionicons } from '@expo/vector-icons';
7
9
 
8
10
  interface ProfileScreenProps extends BaseScreenProps {
@@ -10,22 +12,40 @@ interface ProfileScreenProps extends BaseScreenProps {
10
12
  username?: string;
11
13
  }
12
14
 
13
- const ProfileScreen: React.FC<ProfileScreenProps> = ({ userId, username, theme, goBack }) => {
15
+ const ProfileScreen: React.FC<ProfileScreenProps> = ({ userId, username, theme, goBack, navigate }) => {
14
16
  const { oxyServices, user: currentUser } = useOxy();
15
17
  const [profile, setProfile] = useState<any>(null);
16
18
  const [karmaTotal, setKarmaTotal] = useState<number | null>(null);
17
19
  const [postsCount, setPostsCount] = useState<number | null>(null);
18
20
  const [commentsCount, setCommentsCount] = useState<number | null>(null);
19
- const [followersCount, setFollowersCount] = useState<number | null>(null);
20
- const [followingCount, setFollowingCount] = useState<number | null>(null);
21
21
  const [isLoading, setIsLoading] = useState(true);
22
22
  const [error, setError] = useState<string | null>(null);
23
+ const [links, setLinks] = useState<Array<{
24
+ url: string;
25
+ title?: string;
26
+ description?: string;
27
+ image?: string;
28
+ id: string;
29
+ }>>([]);
30
+
31
+ // Use the follow hook for real follower data
32
+ const {
33
+ followerCount,
34
+ followingCount,
35
+ isLoadingCounts,
36
+ fetchUserCounts,
37
+ setFollowerCount,
38
+ setFollowingCount,
39
+ } = useFollow(userId);
23
40
 
24
41
  const isDarkTheme = theme === 'dark';
25
42
  const backgroundColor = isDarkTheme ? '#121212' : '#FFFFFF';
26
43
  const textColor = isDarkTheme ? '#FFFFFF' : '#000000';
27
44
  const primaryColor = '#d169e5';
28
45
 
46
+ // Check if current user is viewing their own profile
47
+ const isOwnProfile = currentUser && currentUser.id === userId;
48
+
29
49
  useEffect(() => {
30
50
  console.log('ProfileScreen - userId:', userId);
31
51
  console.log('ProfileScreen - username:', username);
@@ -62,12 +82,50 @@ const ProfileScreen: React.FC<ProfileScreenProps> = ({ userId, username, theme,
62
82
  setProfile(profileRes);
63
83
  setKarmaTotal(typeof karmaRes.total === 'number' ? karmaRes.total : null);
64
84
 
65
- // Mock data for other stats
66
- // In a real app, these would come from API endpoints
85
+ // Extract links from profile data
86
+ if (profileRes.linksMetadata && Array.isArray(profileRes.linksMetadata)) {
87
+ const linksWithIds = profileRes.linksMetadata.map((link: any, index: number) => ({
88
+ ...link,
89
+ id: link.id || `existing-${index}`
90
+ }));
91
+ setLinks(linksWithIds);
92
+ } else if (Array.isArray(profileRes.links)) {
93
+ const simpleLinks = profileRes.links.map((l: any) => typeof l === 'string' ? l : l.link).filter(Boolean);
94
+ const linksWithMetadata = simpleLinks.map((url: string, index: number) => ({
95
+ url,
96
+ title: url.replace(/^https?:\/\//, '').replace(/\/$/, ''),
97
+ description: `Link to ${url}`,
98
+ image: undefined,
99
+ id: `existing-${index}`
100
+ }));
101
+ setLinks(linksWithMetadata);
102
+ } else if (profileRes.website) {
103
+ setLinks([{
104
+ url: profileRes.website,
105
+ title: profileRes.website.replace(/^https?:\/\//, '').replace(/\/$/, ''),
106
+ description: `Link to ${profileRes.website}`,
107
+ image: undefined,
108
+ id: 'existing-0'
109
+ }]);
110
+ } else {
111
+ setLinks([]);
112
+ }
113
+
114
+ // Set real follower counts from profile data if available
115
+ if (profileRes._count) {
116
+ setFollowerCount?.(profileRes._count.followers || 0);
117
+ setFollowingCount?.(profileRes._count.following || 0);
118
+ } else if (profileRes.stats) {
119
+ setFollowerCount?.(profileRes.stats.followers || 0);
120
+ setFollowingCount?.(profileRes.stats.following || 0);
121
+ } else {
122
+ // Fallback: fetch counts separately
123
+ fetchUserCounts?.();
124
+ }
125
+
126
+ // Mock data for other stats (these would come from separate API endpoints)
67
127
  setPostsCount(Math.floor(Math.random() * 50));
68
128
  setCommentsCount(Math.floor(Math.random() * 100));
69
- setFollowersCount(Math.floor(Math.random() * 200));
70
- setFollowingCount(Math.floor(Math.random() * 100));
71
129
  })
72
130
  .catch((err: any) => {
73
131
  console.error('Profile loading error:', err);
@@ -135,9 +193,25 @@ const ProfileScreen: React.FC<ProfileScreenProps> = ({ userId, username, theme,
135
193
  <View style={styles.avatarWrapper}>
136
194
  <Avatar uri={profile?.avatar?.url} name={profile?.username || username} size={96} theme={theme} />
137
195
  </View>
138
- {/* Edit Profile/Follow Button placeholder */}
196
+ {/* Conditional Action Button */}
139
197
  <View style={styles.actionButtonWrapper}>
140
- <Text style={styles.actionButton}>Edit Profile</Text>
198
+ {isOwnProfile ? (
199
+ <TouchableOpacity
200
+ style={styles.actionButton}
201
+ onPress={() => navigate?.('EditProfile')}
202
+ >
203
+ <Text style={styles.actionButtonText}>Edit Profile</Text>
204
+ </TouchableOpacity>
205
+ ) : (
206
+ <FollowButton
207
+ userId={userId}
208
+ theme={theme}
209
+ onFollowChange={(isFollowing) => {
210
+ // The follow button will automatically update counts via Zustand
211
+ console.log(`Follow status changed: ${isFollowing}`);
212
+ }}
213
+ />
214
+ )}
141
215
  </View>
142
216
  </View>
143
217
  {/* Profile Info */}
@@ -148,20 +222,67 @@ const ProfileScreen: React.FC<ProfileScreenProps> = ({ userId, username, theme,
148
222
  )}
149
223
  {/* Bio placeholder */}
150
224
  <Text style={[styles.bio, { color: textColor }]}>{profile?.bio || 'This user has no bio yet.'}</Text>
151
- {/* Email and Join Date Row */}
152
- <View style={styles.infoRow}>
153
- {profile?.email && (
154
- <View style={styles.infoItem}>
155
- <Ionicons name="mail-outline" size={16} color={isDarkTheme ? '#BBBBBB' : '#888888'} style={{ marginRight: 4 }} />
156
- <Text style={[styles.infoText, { color: isDarkTheme ? '#BBBBBB' : '#888888' }]}>{profile.email}</Text>
225
+
226
+ {/* Info Grid Row */}
227
+ <View style={styles.infoGrid}>
228
+ {profile?.createdAt && (
229
+ <View style={styles.infoGridItem}>
230
+ <Ionicons name="calendar-outline" size={16} color={isDarkTheme ? '#BBBBBB' : '#888888'} style={{ marginRight: 6 }} />
231
+ <Text style={[styles.infoGridText, { color: isDarkTheme ? '#BBBBBB' : '#888888' }]}>Joined {new Date(profile.createdAt).toLocaleDateString()}</Text>
157
232
  </View>
158
233
  )}
159
- {profile?.createdAt && (
160
- <View style={styles.infoItem}>
161
- <Ionicons name="calendar-outline" size={16} color={isDarkTheme ? '#BBBBBB' : '#888888'} style={{ marginRight: 4 }} />
162
- <Text style={[styles.infoText, { color: isDarkTheme ? '#BBBBBB' : '#888888' }]}>Joined {new Date(profile.createdAt).toLocaleDateString()}</Text>
234
+ {profile?.location && (
235
+ <View style={styles.infoGridItem}>
236
+ <Ionicons name="location-outline" size={16} color={isDarkTheme ? '#BBBBBB' : '#888888'} style={{ marginRight: 6 }} />
237
+ <Text style={[styles.infoGridText, { color: isDarkTheme ? '#BBBBBB' : '#888888' }]} numberOfLines={1}>{profile.location}</Text>
238
+ </View>
239
+ )}
240
+ {profile?.website && (
241
+ <View style={styles.infoGridItem}>
242
+ <Ionicons name="globe-outline" size={16} color={isDarkTheme ? '#BBBBBB' : '#888888'} style={{ marginRight: 6 }} />
243
+ <Text style={[styles.infoGridText, { color: isDarkTheme ? '#BBBBBB' : '#888888' }]} numberOfLines={1}>{profile.website}</Text>
244
+ </View>
245
+ )}
246
+ {profile?.company && (
247
+ <View style={styles.infoGridItem}>
248
+ <Ionicons name="business-outline" size={16} color={isDarkTheme ? '#BBBBBB' : '#888888'} style={{ marginRight: 6 }} />
249
+ <Text style={[styles.infoGridText, { color: isDarkTheme ? '#BBBBBB' : '#888888' }]} numberOfLines={1}>{profile.company}</Text>
250
+ </View>
251
+ )}
252
+ {profile?.jobTitle && (
253
+ <View style={styles.infoGridItem}>
254
+ <Ionicons name="briefcase-outline" size={16} color={isDarkTheme ? '#BBBBBB' : '#888888'} style={{ marginRight: 6 }} />
255
+ <Text style={[styles.infoGridText, { color: isDarkTheme ? '#BBBBBB' : '#888888' }]} numberOfLines={1}>{profile.jobTitle}</Text>
163
256
  </View>
164
257
  )}
258
+ {profile?.education && (
259
+ <View style={styles.infoGridItem}>
260
+ <Ionicons name="school-outline" size={16} color={isDarkTheme ? '#BBBBBB' : '#888888'} style={{ marginRight: 6 }} />
261
+ <Text style={[styles.infoGridText, { color: isDarkTheme ? '#BBBBBB' : '#888888' }]} numberOfLines={1}>{profile.education}</Text>
262
+ </View>
263
+ )}
264
+ {profile?.birthday && (
265
+ <View style={styles.infoGridItem}>
266
+ <Ionicons name="gift-outline" size={16} color={isDarkTheme ? '#BBBBBB' : '#888888'} style={{ marginRight: 6 }} />
267
+ <Text style={[styles.infoGridText, { color: isDarkTheme ? '#BBBBBB' : '#888888' }]}>Born {new Date(profile.birthday).toLocaleDateString()}</Text>
268
+ </View>
269
+ )}
270
+ {links.length > 0 && (
271
+ <TouchableOpacity
272
+ style={styles.infoGridItem}
273
+ onPress={() => navigate?.('UserLinks', { userId, links })}
274
+ >
275
+ <Ionicons name="link-outline" size={16} color={isDarkTheme ? '#BBBBBB' : '#888888'} style={{ marginRight: 6 }} />
276
+ <Text style={[styles.infoGridText, { color: isDarkTheme ? '#BBBBBB' : '#888888' }]} numberOfLines={1}>
277
+ {links[0].url}
278
+ </Text>
279
+ {links.length > 1 && (
280
+ <Text style={[styles.linksMore, { color: isDarkTheme ? '#BBBBBB' : '#888888' }]}>
281
+ + {links.length - 1} more
282
+ </Text>
283
+ )}
284
+ </TouchableOpacity>
285
+ )}
165
286
  </View>
166
287
  {/* Divider */}
167
288
  <View style={styles.divider} />
@@ -172,11 +293,19 @@ const ProfileScreen: React.FC<ProfileScreenProps> = ({ userId, username, theme,
172
293
  <Text style={[styles.karmaLabel, { color: isDarkTheme ? '#BBBBBB' : '#888888' }]}>Karma</Text>
173
294
  </View>
174
295
  <View style={styles.statItem}>
175
- <Text style={[styles.karmaAmount, { color: textColor }]}>{followersCount !== null ? followersCount : '--'}</Text>
296
+ {isLoadingCounts ? (
297
+ <ActivityIndicator size="small" color={textColor} />
298
+ ) : (
299
+ <Text style={[styles.karmaAmount, { color: textColor }]}>{followerCount !== null ? followerCount : '--'}</Text>
300
+ )}
176
301
  <Text style={[styles.karmaLabel, { color: isDarkTheme ? '#BBBBBB' : '#888888' }]}>Followers</Text>
177
302
  </View>
178
303
  <View style={styles.statItem}>
179
- <Text style={[styles.karmaAmount, { color: textColor }]}>{followingCount !== null ? followingCount : '--'}</Text>
304
+ {isLoadingCounts ? (
305
+ <ActivityIndicator size="small" color={textColor} />
306
+ ) : (
307
+ <Text style={[styles.karmaAmount, { color: textColor }]}>{followingCount !== null ? followingCount : '--'}</Text>
308
+ )}
180
309
  <Text style={[styles.karmaLabel, { color: isDarkTheme ? '#BBBBBB' : '#888888' }]}>Following</Text>
181
310
  </View>
182
311
  </View>
@@ -194,15 +323,49 @@ const styles = StyleSheet.create({
194
323
  avatarRow: { flexDirection: 'row', alignItems: 'flex-end', marginTop: -56, paddingHorizontal: 20, justifyContent: 'space-between', zIndex: 2 },
195
324
  avatarWrapper: { borderWidth: 5, borderColor: '#fff', borderRadius: 64, overflow: 'hidden', backgroundColor: '#fff', },
196
325
  actionButtonWrapper: { flex: 1, alignItems: 'flex-end', justifyContent: 'flex-end' },
197
- actionButton: { backgroundColor: '#fff', color: '#d169e5', borderWidth: 1, borderColor: '#d169e5', borderRadius: 24, paddingVertical: 7, paddingHorizontal: 22, fontWeight: 'bold', fontSize: 16, marginBottom: 8, elevation: 2, shadowColor: '#d169e5', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.08, shadowRadius: 2 },
198
- header: { alignItems: 'flex-start', paddingTop: 18, paddingBottom: 24, width: '100%', paddingHorizontal: 20 },
326
+ actionButton: {
327
+ backgroundColor: '#fff',
328
+ borderWidth: 1,
329
+ borderColor: '#d169e5',
330
+ borderRadius: 24,
331
+ paddingVertical: 7,
332
+ paddingHorizontal: 22,
333
+ marginBottom: 8,
334
+ elevation: 2,
335
+ shadowColor: '#d169e5',
336
+ shadowOffset: { width: 0, height: 1 },
337
+ shadowOpacity: 0.08,
338
+ shadowRadius: 2
339
+ },
340
+ actionButtonText: {
341
+ color: '#d169e5',
342
+ fontWeight: 'bold',
343
+ fontSize: 16
344
+ },
345
+ header: { alignItems: 'flex-start', width: '100%', paddingHorizontal: 20 },
199
346
  displayName: { fontSize: 24, fontWeight: 'bold', marginTop: 10, marginBottom: 2, letterSpacing: 0.1 },
200
347
  subText: { fontSize: 16, marginBottom: 2, color: '#a0a0a0' },
201
- bio: { fontSize: 16, marginTop: 10, marginBottom: 10, color: '#666', fontStyle: 'italic', lineHeight: 22 },
202
- infoRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 10, flexWrap: 'wrap' },
203
- infoItem: { flexDirection: 'row', alignItems: 'center', marginRight: 28, marginBottom: 4, minWidth: 120 },
204
- infoText: { fontSize: 15 },
348
+ bio: { fontSize: 16, marginTop: 10, marginBottom: 10, color: '#666', lineHeight: 22 },
349
+ infoGrid: {
350
+ flexDirection: 'row',
351
+ alignItems: 'center',
352
+ marginBottom: 10,
353
+ flexWrap: 'wrap'
354
+ },
355
+ infoGridItem: {
356
+ flexDirection: 'row',
357
+ alignItems: 'center',
358
+ marginRight: 24,
359
+ marginBottom: 4
360
+ },
361
+ infoGridText: {
362
+ fontSize: 15
363
+ },
205
364
  divider: { height: 1, backgroundColor: '#e0e0e0', width: '100%', marginVertical: 14 },
365
+ linksMore: {
366
+ fontSize: 15,
367
+ marginLeft: 4
368
+ },
206
369
  statsRow: { width: '100%', flex: 1, flexDirection: 'row', alignItems: 'center', marginTop: 6, marginBottom: 2, justifyContent: 'space-between' },
207
370
  statItem: { flex: 1, alignItems: 'center', minWidth: 50, marginBottom: 12 },
208
371
  karmaLabel: { fontSize: 14, marginBottom: 2, textAlign: 'center', color: '#a0a0a0' },
@@ -0,0 +1,96 @@
1
+ import React from 'react';
2
+ import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Image, Linking } from 'react-native';
3
+ import { Ionicons } from '@expo/vector-icons';
4
+ import { BaseScreenProps } from '../navigation/types';
5
+ import { Header, GroupedSection } from '../components';
6
+
7
+ interface UserLinksScreenProps extends BaseScreenProps {
8
+ userId: string;
9
+ links: Array<{
10
+ url: string;
11
+ title?: string;
12
+ description?: string;
13
+ image?: string;
14
+ id: string;
15
+ }>;
16
+ }
17
+
18
+ const UserLinksScreen: React.FC<UserLinksScreenProps> = ({
19
+ userId,
20
+ links,
21
+ theme,
22
+ goBack,
23
+ navigate
24
+ }) => {
25
+ const isDarkTheme = theme === 'dark';
26
+ const themeStyles = {
27
+ backgroundColor: isDarkTheme ? '#000' : '#f2f2f2',
28
+ textColor: isDarkTheme ? '#fff' : '#333',
29
+ primaryColor: '#007AFF',
30
+ };
31
+
32
+ const handleLinkPress = async (url: string) => {
33
+ try {
34
+ await Linking.openURL(url);
35
+ } catch (error) {
36
+ console.error('Error opening link:', error);
37
+ }
38
+ };
39
+
40
+ const groupedItems = links.map((link) => ({
41
+ id: link.id,
42
+ icon: link.image ? undefined : 'link',
43
+ iconColor: '#32D74B',
44
+ image: link.image || undefined,
45
+ imageSize: 40,
46
+ title: link.title || link.url,
47
+ subtitle: link.description || link.url,
48
+ onPress: () => handleLinkPress(link.url),
49
+ multiRow: true,
50
+ }));
51
+
52
+ return (
53
+ <View style={[styles.container, { backgroundColor: themeStyles.backgroundColor }]}>
54
+ <Header
55
+ title="Links"
56
+ subtitle={`${links.length} link${links.length !== 1 ? 's' : ''}`}
57
+ theme={theme}
58
+ onBack={goBack}
59
+ elevation="subtle"
60
+ />
61
+
62
+ <ScrollView style={styles.content}>
63
+ <View style={styles.section}>
64
+ <Text style={[styles.sectionTitle, { color: themeStyles.textColor }]}>Links</Text>
65
+
66
+ <GroupedSection
67
+ items={groupedItems}
68
+ theme={theme}
69
+ />
70
+ </View>
71
+ </ScrollView>
72
+ </View>
73
+ );
74
+ };
75
+
76
+ const styles = StyleSheet.create({
77
+ container: {
78
+ flex: 1,
79
+ backgroundColor: '#f2f2f2',
80
+ },
81
+ content: {
82
+ flex: 1,
83
+ padding: 16,
84
+ },
85
+ section: {
86
+ marginBottom: 24,
87
+ },
88
+ sectionTitle: {
89
+ fontSize: 16,
90
+ fontWeight: '600',
91
+ color: '#333',
92
+ marginBottom: 12,
93
+ },
94
+ });
95
+
96
+ export default UserLinksScreen;
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { View, Text, StyleSheet, ScrollView, Platform } from 'react-native';
3
3
  import { BaseScreenProps } from '../../navigation/types';
4
+ import { Header } from '../../components';
4
5
 
5
6
  const KarmaAboutScreen: React.FC<BaseScreenProps> = ({ goBack, theme }) => {
6
7
  const isDarkTheme = theme === 'dark';
@@ -10,7 +11,13 @@ const KarmaAboutScreen: React.FC<BaseScreenProps> = ({ goBack, theme }) => {
10
11
 
11
12
  return (
12
13
  <View style={[styles.container, { backgroundColor }]}>
13
- <Text style={[styles.title, { color: textColor }]}>About Karma</Text>
14
+ <Header
15
+ title="About Karma"
16
+ subtitle="Learn about the karma system"
17
+ theme={theme}
18
+ onBack={goBack}
19
+ elevation="subtle"
20
+ />
14
21
  <ScrollView contentContainerStyle={styles.contentContainer}>
15
22
  <Text style={[styles.paragraph, { color: textColor }]}>Karma is a recognition of your positive actions in the Oxy Ecosystem. It cannot be sent or received directly, only earned by contributing to the community.</Text>
16
23
  <Text style={[styles.section, { color: primaryColor }]}>How to Earn Karma</Text>
@@ -37,7 +44,7 @@ const styles = StyleSheet.create({
37
44
  margin: 24,
38
45
  marginBottom: 24,
39
46
  },
40
- contentContainer: { padding: 24 },
47
+ contentContainer: { padding: 24, paddingTop: 20 },
41
48
  section: { fontSize: 18, fontWeight: 'bold', marginTop: 24, marginBottom: 8 },
42
49
  paragraph: { fontSize: 16, marginBottom: 12 },
43
50
  });
@@ -16,7 +16,6 @@ import Avatar from '../../components/Avatar';
16
16
  import { Ionicons } from '@expo/vector-icons';
17
17
 
18
18
  const KarmaCenterScreen: React.FC<BaseScreenProps> = ({
19
- onClose,
20
19
  theme,
21
20
  navigate,
22
21
  goBack,
@@ -137,11 +136,6 @@ const KarmaCenterScreen: React.FC<BaseScreenProps> = ({
137
136
  </View>
138
137
  {error && <Text style={{ color: '#D32F2F', marginTop: 16, textAlign: 'center' }}>{error}</Text>}
139
138
  </ScrollView>
140
- <View style={styles.footer}>
141
- <TouchableOpacity style={styles.closeButton} onPress={onClose}>
142
- <Text style={[styles.closeButtonText, { color: primaryColor }]}>Close</Text>
143
- </TouchableOpacity>
144
- </View>
145
139
  </View>
146
140
  );
147
141
  };
@@ -247,20 +241,7 @@ const styles = StyleSheet.create({
247
241
  fontSize: 13,
248
242
  marginTop: 2,
249
243
  },
250
- footer: {
251
- padding: 16,
252
- borderTopWidth: 1,
253
- borderTopColor: '#E0E0E0',
254
- alignItems: 'center',
255
- },
256
- closeButton: {
257
- paddingVertical: 8,
258
- paddingHorizontal: 16,
259
- },
260
- closeButtonText: {
261
- fontSize: 16,
262
- fontWeight: '600',
263
- },
244
+
264
245
  message: {
265
246
  fontSize: 16,
266
247
  textAlign: 'center',