@oxyhq/services 5.1.12 → 5.1.13

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 (102) hide show
  1. package/lib/commonjs/ui/components/OxyProvider.js +50 -30
  2. package/lib/commonjs/ui/components/OxyProvider.js.map +1 -1
  3. package/lib/commonjs/ui/components/OxySignInButton.js +4 -4
  4. package/lib/commonjs/ui/components/OxySignInButton.js.map +1 -1
  5. package/lib/commonjs/ui/context/OxyContext.js +15 -7
  6. package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
  7. package/lib/commonjs/ui/navigation/OxyRouter.js +47 -7
  8. package/lib/commonjs/ui/navigation/OxyRouter.js.map +1 -1
  9. package/lib/commonjs/ui/screens/ProfileScreen.js +346 -0
  10. package/lib/commonjs/ui/screens/ProfileScreen.js.map +1 -0
  11. package/lib/commonjs/ui/screens/SignInScreen.js +82 -86
  12. package/lib/commonjs/ui/screens/SignInScreen.js.map +1 -1
  13. package/lib/commonjs/ui/screens/SignUpScreen.js +194 -103
  14. package/lib/commonjs/ui/screens/SignUpScreen.js.map +1 -1
  15. package/lib/commonjs/ui/screens/karma/KarmaAboutScreen.js +88 -0
  16. package/lib/commonjs/ui/screens/karma/KarmaAboutScreen.js.map +1 -0
  17. package/lib/commonjs/ui/screens/karma/KarmaCenterScreen.js +364 -0
  18. package/lib/commonjs/ui/screens/karma/KarmaCenterScreen.js.map +1 -0
  19. package/lib/commonjs/ui/screens/karma/KarmaFAQScreen.js +202 -0
  20. package/lib/commonjs/ui/screens/karma/KarmaFAQScreen.js.map +1 -0
  21. package/lib/commonjs/ui/screens/karma/KarmaLeaderboardScreen.js +148 -0
  22. package/lib/commonjs/ui/screens/karma/KarmaLeaderboardScreen.js.map +1 -0
  23. package/lib/commonjs/ui/screens/karma/KarmaRewardsScreen.js +127 -0
  24. package/lib/commonjs/ui/screens/karma/KarmaRewardsScreen.js.map +1 -0
  25. package/lib/commonjs/ui/screens/karma/KarmaRulesScreen.js +105 -0
  26. package/lib/commonjs/ui/screens/karma/KarmaRulesScreen.js.map +1 -0
  27. package/lib/commonjs/ui/styles/theme.js +1 -2
  28. package/lib/commonjs/ui/styles/theme.js.map +1 -1
  29. package/lib/module/ui/components/OxyProvider.js +51 -31
  30. package/lib/module/ui/components/OxyProvider.js.map +1 -1
  31. package/lib/module/ui/components/OxySignInButton.js +4 -4
  32. package/lib/module/ui/components/OxySignInButton.js.map +1 -1
  33. package/lib/module/ui/context/OxyContext.js +15 -7
  34. package/lib/module/ui/context/OxyContext.js.map +1 -1
  35. package/lib/module/ui/navigation/OxyRouter.js +47 -7
  36. package/lib/module/ui/navigation/OxyRouter.js.map +1 -1
  37. package/lib/module/ui/screens/ProfileScreen.js +340 -0
  38. package/lib/module/ui/screens/ProfileScreen.js.map +1 -0
  39. package/lib/module/ui/screens/SignInScreen.js +83 -87
  40. package/lib/module/ui/screens/SignInScreen.js.map +1 -1
  41. package/lib/module/ui/screens/SignUpScreen.js +194 -104
  42. package/lib/module/ui/screens/SignUpScreen.js.map +1 -1
  43. package/lib/module/ui/screens/karma/KarmaAboutScreen.js +83 -0
  44. package/lib/module/ui/screens/karma/KarmaAboutScreen.js.map +1 -0
  45. package/lib/module/ui/screens/karma/KarmaCenterScreen.js +358 -0
  46. package/lib/module/ui/screens/karma/KarmaCenterScreen.js.map +1 -0
  47. package/lib/module/ui/screens/karma/KarmaFAQScreen.js +197 -0
  48. package/lib/module/ui/screens/karma/KarmaFAQScreen.js.map +1 -0
  49. package/lib/module/ui/screens/karma/KarmaLeaderboardScreen.js +142 -0
  50. package/lib/module/ui/screens/karma/KarmaLeaderboardScreen.js.map +1 -0
  51. package/lib/module/ui/screens/karma/KarmaRewardsScreen.js +122 -0
  52. package/lib/module/ui/screens/karma/KarmaRewardsScreen.js.map +1 -0
  53. package/lib/module/ui/screens/karma/KarmaRulesScreen.js +100 -0
  54. package/lib/module/ui/screens/karma/KarmaRulesScreen.js.map +1 -0
  55. package/lib/module/ui/styles/theme.js +1 -2
  56. package/lib/module/ui/styles/theme.js.map +1 -1
  57. package/lib/typescript/ui/components/OxyProvider.d.ts.map +1 -1
  58. package/lib/typescript/ui/components/OxySignInButton.d.ts +5 -0
  59. package/lib/typescript/ui/components/OxySignInButton.d.ts.map +1 -1
  60. package/lib/typescript/ui/context/OxyContext.d.ts +4 -1
  61. package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
  62. package/lib/typescript/ui/navigation/OxyRouter.d.ts.map +1 -1
  63. package/lib/typescript/ui/screens/ProfileScreen.d.ts +9 -0
  64. package/lib/typescript/ui/screens/ProfileScreen.d.ts.map +1 -0
  65. package/lib/typescript/ui/screens/SignInScreen.d.ts.map +1 -1
  66. package/lib/typescript/ui/screens/SignUpScreen.d.ts.map +1 -1
  67. package/lib/typescript/ui/screens/karma/KarmaAboutScreen.d.ts +5 -0
  68. package/lib/typescript/ui/screens/karma/KarmaAboutScreen.d.ts.map +1 -0
  69. package/lib/typescript/ui/screens/karma/KarmaCenterScreen.d.ts +5 -0
  70. package/lib/typescript/ui/screens/karma/KarmaCenterScreen.d.ts.map +1 -0
  71. package/lib/typescript/ui/screens/karma/KarmaFAQScreen.d.ts +5 -0
  72. package/lib/typescript/ui/screens/karma/KarmaFAQScreen.d.ts.map +1 -0
  73. package/lib/typescript/ui/screens/karma/KarmaLeaderboardScreen.d.ts +5 -0
  74. package/lib/typescript/ui/screens/karma/KarmaLeaderboardScreen.d.ts.map +1 -0
  75. package/lib/typescript/ui/screens/karma/KarmaRewardsScreen.d.ts +5 -0
  76. package/lib/typescript/ui/screens/karma/KarmaRewardsScreen.d.ts.map +1 -0
  77. package/lib/typescript/ui/screens/karma/KarmaRulesScreen.d.ts +5 -0
  78. package/lib/typescript/ui/screens/karma/KarmaRulesScreen.d.ts.map +1 -0
  79. package/lib/typescript/ui/styles/theme.d.ts +0 -1
  80. package/lib/typescript/ui/styles/theme.d.ts.map +1 -1
  81. package/package.json +4 -3
  82. package/src/ui/components/OxyProvider.tsx +42 -29
  83. package/src/ui/components/OxySignInButton.tsx +9 -3
  84. package/src/ui/context/OxyContext.tsx +16 -8
  85. package/src/ui/navigation/OxyRouter.tsx +44 -7
  86. package/src/ui/screens/ProfileScreen.tsx +155 -0
  87. package/src/ui/screens/SignInScreen.tsx +68 -73
  88. package/src/ui/screens/SignUpScreen.tsx +140 -81
  89. package/src/ui/screens/karma/KarmaAboutScreen.tsx +45 -0
  90. package/src/ui/screens/karma/KarmaCenterScreen.tsx +271 -0
  91. package/src/ui/screens/karma/KarmaFAQScreen.tsx +164 -0
  92. package/src/ui/screens/karma/KarmaLeaderboardScreen.tsx +79 -0
  93. package/src/ui/screens/karma/KarmaRewardsScreen.tsx +55 -0
  94. package/src/ui/screens/karma/KarmaRulesScreen.tsx +65 -0
  95. package/src/ui/styles/theme.ts +1 -2
  96. package/lib/commonjs/ui/screens/AboutKarmaScreen.js +0 -50
  97. package/lib/commonjs/ui/screens/AboutKarmaScreen.js.map +0 -1
  98. package/lib/module/ui/screens/AboutKarmaScreen.js +0 -45
  99. package/lib/module/ui/screens/AboutKarmaScreen.js.map +0 -1
  100. package/lib/typescript/ui/screens/AboutKarmaScreen.d.ts +0 -5
  101. package/lib/typescript/ui/screens/AboutKarmaScreen.d.ts.map +0 -1
  102. package/src/ui/screens/AboutKarmaScreen.tsx +0 -58
@@ -14,6 +14,7 @@ import { FontLoader, setupFonts } from './FontLoader';
14
14
 
15
15
  // Import bottom sheet components directly - no longer a peer dependency
16
16
  import { BottomSheetModal, BottomSheetBackdrop, BottomSheetBackdropProps, BottomSheetModalProvider, BottomSheetView } from './bottomSheet';
17
+ import { BottomSheetScrollView } from '@gorhom/bottom-sheet';
17
18
 
18
19
  // Initialize fonts automatically
19
20
  setupFonts();
@@ -95,6 +96,10 @@ const OxyBottomSheet: React.FC<OxyProviderProps> = ({
95
96
  // Use the internal ref (which is passed as a prop from OxyProvider)
96
97
  const modalRef = useRef<BottomSheetModal>(null);
97
98
 
99
+ // Track content height for dynamic sizing
100
+ const [contentHeight, setContentHeight] = useState<number>(0);
101
+ const screenHeight = Dimensions.get('window').height;
102
+
98
103
  // Set up effect to sync the internal ref with our modal ref
99
104
  useEffect(() => {
100
105
  if (bottomSheetRef && modalRef.current) {
@@ -116,18 +121,18 @@ const OxyBottomSheet: React.FC<OxyProviderProps> = ({
116
121
 
117
122
  // Add a method to navigate between screens
118
123
  // @ts-ignore - Adding custom method
119
- bottomSheetRef.current._navigateToScreen = (screenName: string) => {
124
+ bottomSheetRef.current._navigateToScreen = (screenName: string, props?: Record<string, any>) => {
120
125
  // Access the navigation function exposed by OxyRouter
121
126
  // Use internal mechanism to notify router about navigation
122
127
  // We'll use a simple event-based approach
123
128
  if (typeof document !== 'undefined') {
124
129
  // For web - use a custom event
125
- const event = new CustomEvent('oxy:navigate', { detail: screenName });
130
+ const event = new CustomEvent('oxy:navigate', { detail: { screen: screenName, props } });
126
131
  document.dispatchEvent(event);
127
132
  } else {
128
133
  // For React Native - use the navigation prop directly if available
129
- console.log(`Requesting navigation to ${screenName}`);
130
- // We'll implement a simpler mechanism in OxyRouter
134
+ // We'll implement a mechanism in OxyRouter to listen for this
135
+ (global as any).oxyNavigateEvent = { screen: screenName, props };
131
136
  }
132
137
  };
133
138
  }
@@ -300,22 +305,37 @@ const OxyBottomSheet: React.FC<OxyProviderProps> = ({
300
305
  const highestPoint = points[points.length - 1];
301
306
  setSnapPoints([highestPoint, highestPoint]);
302
307
  } else {
303
- setSnapPoints(points);
308
+ // If we have content height, use it as a constraint
309
+ if (contentHeight > 0) {
310
+ // Calculate content height as percentage of screen (plus some padding)
311
+ const contentHeightPercent = Math.min(Math.ceil((contentHeight + 40) / screenHeight * 100), 90) + '%';
312
+ // Use content height for first snap point if it's taller than the default
313
+ const firstPoint = contentHeight / screenHeight > 0.6 ? contentHeightPercent : points[0];
314
+ setSnapPoints([firstPoint, points[1]]);
315
+ } else {
316
+ setSnapPoints(points);
317
+ }
304
318
  }
305
- }, [keyboardVisible]);
306
-
307
- // Method to programmatically navigate to a specific screen
308
- const navigateToScreen = useCallback((screenName: string) => {
309
- // If we have a router component with navigate method, use it
310
- if (modalRef.current) {
311
- // Store the navigate function on the modal ref so it can be accessed externally
312
- // @ts-ignore - Adding custom property for external navigation
313
- modalRef.current._navigateToScreen = (screen: string) => {
314
- // This will be populated by the OxyRouter component when it renders
315
- console.log(`Navigating to ${screen} programmatically`);
316
- };
319
+ }, [keyboardVisible, contentHeight, screenHeight]);
320
+
321
+ // Handle content layout changes to measure height
322
+ const handleContentLayout = useCallback((event: any) => {
323
+ const layoutHeight = event.nativeEvent.layout.height;
324
+ setContentHeight(layoutHeight);
325
+
326
+ // Update snap points based on new content height
327
+ if (keyboardVisible) {
328
+ // If keyboard is visible, use the highest snap point
329
+ const highestPoint = snapPoints[snapPoints.length - 1];
330
+ setSnapPoints([highestPoint, highestPoint]);
331
+ } else {
332
+ if (layoutHeight > 0) {
333
+ const contentHeightPercent = Math.min(Math.ceil((layoutHeight + 40) / screenHeight * 100), 90) + '%';
334
+ const firstPoint = layoutHeight / screenHeight > 0.6 ? contentHeightPercent : snapPoints[0];
335
+ setSnapPoints([firstPoint, snapPoints[1]]);
336
+ }
317
337
  }
318
- }, []);
338
+ }, [keyboardVisible, screenHeight, snapPoints]);
319
339
 
320
340
  // Close the bottom sheet with animation
321
341
  const handleClose = useCallback(() => {
@@ -367,6 +387,7 @@ const OxyBottomSheet: React.FC<OxyProviderProps> = ({
367
387
  snapPoints={snapPoints}
368
388
  enablePanDownToClose
369
389
  backdropComponent={renderBackdrop}
390
+ // Remove enableDynamicSizing as we're implementing our own solution
370
391
  handleComponent={() => (
371
392
  <Animated.View
372
393
  style={{
@@ -410,16 +431,13 @@ const OxyBottomSheet: React.FC<OxyProviderProps> = ({
410
431
  console.log(`Animating from index ${fromIndex} to ${toIndex}`);
411
432
  }}
412
433
  >
413
- <BottomSheetView
434
+ <BottomSheetScrollView
414
435
  style={[
415
436
  styles.contentContainer,
416
437
  // Override padding if provided in customStyles
417
438
  customStyles.contentPadding !== undefined && { padding: customStyles.contentPadding },
418
- // Add bottom padding when keyboard is visible to ensure content is not covered
419
- keyboardVisible && {
420
- paddingBottom: Math.max(keyboardHeight - insets.bottom, 0)
421
- },
422
439
  ]}
440
+ onLayout={handleContentLayout}
423
441
  >
424
442
  <Animated.View
425
443
  style={[
@@ -443,23 +461,18 @@ const OxyBottomSheet: React.FC<OxyProviderProps> = ({
443
461
  adjustSnapPoints={adjustSnapPoints}
444
462
  />
445
463
  </Animated.View>
446
- </BottomSheetView>
464
+ </BottomSheetScrollView>
447
465
  </BottomSheetModal>
448
466
  );
449
467
  };
450
468
 
451
469
  const styles = StyleSheet.create({
452
470
  contentContainer: {
453
- flex: 1,
454
- padding: 16,
455
471
  width: '100%',
456
- height: '100%',
457
472
  backgroundColor: 'transparent', // Make this transparent to let the bottom sheet background show through
458
473
  },
459
474
  animatedContent: {
460
- flex: 1,
461
475
  width: '100%',
462
- height: '100%',
463
476
  },
464
477
  indicator: {
465
478
  width: 40,
@@ -44,6 +44,12 @@ export interface OxySignInButtonProps {
44
44
  * @default false
45
45
  */
46
46
  showWhenAuthenticated?: boolean;
47
+
48
+ /**
49
+ * Which screen to open in the bottom sheet
50
+ * @default 'SignIn'
51
+ */
52
+ screen?: string;
47
53
  }
48
54
 
49
55
  /**
@@ -79,6 +85,7 @@ export const OxySignInButton: React.FC<OxySignInButtonProps> = ({
79
85
  text = 'Sign in with Oxy',
80
86
  disabled = false,
81
87
  showWhenAuthenticated = false,
88
+ screen = 'SignIn',
82
89
  }) => {
83
90
  // Get all needed values from context in a single call
84
91
  const { user, showBottomSheet } = useOxy();
@@ -93,10 +100,9 @@ export const OxySignInButton: React.FC<OxySignInButtonProps> = ({
93
100
  return;
94
101
  }
95
102
 
96
- // Default behavior: open the bottom sheet and navigate to SignIn
103
+ // Allow passing any screen name, including 'SignUp', 'AccountCenter', etc.
97
104
  if (showBottomSheet) {
98
- // Show the bottom sheet and navigate to SignIn
99
- showBottomSheet('SignIn');
105
+ showBottomSheet(screen);
100
106
  } else {
101
107
  console.warn('OxySignInButton: showBottomSheet is not available. Either provide an onPress prop or ensure this component is used within an OxyProvider.');
102
108
  }
@@ -20,7 +20,7 @@ export interface OxyContextState {
20
20
  bottomSheetRef?: React.RefObject<any>;
21
21
 
22
22
  // Methods to directly control the bottom sheet
23
- showBottomSheet?: (screen?: string) => void;
23
+ showBottomSheet?: (screenOrConfig?: string | { screen: string; props?: Record<string, any> }) => void;
24
24
  hideBottomSheet?: () => void;
25
25
  }
26
26
 
@@ -292,16 +292,24 @@ export const OxyContextProvider: React.FC<OxyContextProviderProps> = ({
292
292
  };
293
293
 
294
294
  // Methods to control the bottom sheet
295
- const showBottomSheet = useCallback((screen?: string) => {
295
+ const showBottomSheet = useCallback((screenOrConfig?: string | { screen: string; props?: Record<string, any> }) => {
296
296
  if (bottomSheetRef?.current) {
297
297
  // Expand the bottom sheet
298
298
  bottomSheetRef.current.expand();
299
-
300
- // If a screen is specified, navigate to it
301
- if (screen && bottomSheetRef.current._navigateToScreen) {
302
- setTimeout(() => {
303
- bottomSheetRef.current._navigateToScreen(screen);
304
- }, 100); // Small delay to ensure the sheet is expanded first
299
+ if (typeof screenOrConfig === 'string') {
300
+ // If a screen is specified, navigate to it
301
+ if (screenOrConfig && bottomSheetRef.current._navigateToScreen) {
302
+ setTimeout(() => {
303
+ bottomSheetRef.current._navigateToScreen(screenOrConfig);
304
+ }, 100);
305
+ }
306
+ } else if (screenOrConfig && typeof screenOrConfig === 'object' && screenOrConfig.screen) {
307
+ // If an object is passed, navigate and pass props
308
+ if (bottomSheetRef.current._navigateToScreen) {
309
+ setTimeout(() => {
310
+ bottomSheetRef.current._navigateToScreen(screenOrConfig.screen, screenOrConfig.props || {});
311
+ }, 100);
312
+ }
305
313
  }
306
314
  }
307
315
  }, [bottomSheetRef]);
@@ -7,7 +7,13 @@ import SignInScreen from '../screens/SignInScreen';
7
7
  import SignUpScreen from '../screens/SignUpScreen';
8
8
  import AccountCenterScreen from '../screens/AccountCenterScreen';
9
9
  import AccountOverviewScreen from '../screens/AccountOverviewScreen';
10
- import AboutKarmaScreen from '../screens/AboutKarmaScreen';
10
+ import KarmaCenterScreen from '../screens/karma/KarmaCenterScreen';
11
+ import KarmaLeaderboardScreen from '../screens/karma/KarmaLeaderboardScreen';
12
+ import KarmaRulesScreen from '../screens/karma/KarmaRulesScreen';
13
+ import KarmaAboutScreen from '../screens/karma/KarmaAboutScreen';
14
+ import KarmaRewardsScreen from '../screens/karma/KarmaRewardsScreen';
15
+ import KarmaFAQScreen from '../screens/karma/KarmaFAQScreen';
16
+ import ProfileScreen from '../screens/ProfileScreen';
11
17
 
12
18
  // Import types
13
19
  import { OxyRouterProps, RouteConfig } from './types';
@@ -16,11 +22,11 @@ import { OxyRouterProps, RouteConfig } from './types';
16
22
  const routes: Record<string, RouteConfig> = {
17
23
  SignIn: {
18
24
  component: SignInScreen,
19
- snapPoints: ['60%', '80%'],
25
+ snapPoints: ['10%', '80%'],
20
26
  },
21
27
  SignUp: {
22
28
  component: SignUpScreen,
23
- snapPoints: ['70%', '90%'],
29
+ snapPoints: ['10%', '90%'],
24
30
  },
25
31
  AccountCenter: {
26
32
  component: AccountCenterScreen,
@@ -30,8 +36,32 @@ const routes: Record<string, RouteConfig> = {
30
36
  component: AccountOverviewScreen,
31
37
  snapPoints: ['60%', '85%'],
32
38
  },
39
+ KarmaCenter: {
40
+ component: KarmaCenterScreen,
41
+ snapPoints: ['60%', '100%'],
42
+ },
43
+ KarmaLeaderboard: {
44
+ component: KarmaLeaderboardScreen,
45
+ snapPoints: ['60%', '100%'],
46
+ },
47
+ KarmaRules: {
48
+ component: KarmaRulesScreen,
49
+ snapPoints: ['60%', '90%'],
50
+ },
33
51
  AboutKarma: {
34
- component: AboutKarmaScreen,
52
+ component: KarmaAboutScreen,
53
+ snapPoints: ['60%', '90%'],
54
+ },
55
+ KarmaRewards: {
56
+ component: KarmaRewardsScreen,
57
+ snapPoints: ['60%', '90%'],
58
+ },
59
+ KarmaFAQ: {
60
+ component: KarmaFAQScreen,
61
+ snapPoints: ['60%', '90%'],
62
+ },
63
+ Profile: {
64
+ component: ProfileScreen,
35
65
  snapPoints: ['60%', '90%'],
36
66
  },
37
67
  };
@@ -69,9 +99,16 @@ const OxyRouter: React.FC<OxyRouterProps> = ({
69
99
  // Set up event listener for navigation events
70
100
  const handleNavigationEvent = (event: any) => {
71
101
  if (event && event.detail) {
72
- const screenName = event.detail;
73
- console.log(`Navigation event received for screen: ${screenName}`);
74
- navigate(screenName);
102
+ // Support both string and object detail
103
+ if (typeof event.detail === 'string') {
104
+ const screenName = event.detail;
105
+ console.log(`Navigation event received for screen: ${screenName}`);
106
+ navigate(screenName);
107
+ } else if (typeof event.detail === 'object' && event.detail.screen) {
108
+ const { screen, props } = event.detail;
109
+ console.log(`Navigation event received for screen: ${screen} with props`, props);
110
+ navigate(screen, props || {});
111
+ }
75
112
  }
76
113
  };
77
114
 
@@ -0,0 +1,155 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { View, Text, StyleSheet, ActivityIndicator, ScrollView } from 'react-native';
3
+ import { BaseScreenProps } from '../navigation/types';
4
+ import { useOxy } from '../context/OxyContext';
5
+ import Avatar from '../components/Avatar';
6
+ import { Ionicons } from '@expo/vector-icons';
7
+
8
+ interface ProfileScreenProps extends BaseScreenProps {
9
+ userId: string;
10
+ username?: string;
11
+ }
12
+
13
+ const ProfileScreen: React.FC<ProfileScreenProps> = ({ userId, username, theme, goBack }) => {
14
+ const { oxyServices } = useOxy();
15
+ const [profile, setProfile] = useState<any>(null);
16
+ const [karmaTotal, setKarmaTotal] = useState<number | null>(null);
17
+ const [postsCount, setPostsCount] = useState<number | null>(null);
18
+ const [commentsCount, setCommentsCount] = useState<number | null>(null);
19
+ const [followersCount, setFollowersCount] = useState<number | null>(null);
20
+ const [followingCount, setFollowingCount] = useState<number | null>(null);
21
+ const [isLoading, setIsLoading] = useState(true);
22
+ const [error, setError] = useState<string | null>(null);
23
+
24
+ const isDarkTheme = theme === 'dark';
25
+ const backgroundColor = isDarkTheme ? '#121212' : '#FFFFFF';
26
+ const textColor = isDarkTheme ? '#FFFFFF' : '#000000';
27
+ const primaryColor = '#d169e5';
28
+
29
+ useEffect(() => {
30
+ setIsLoading(true);
31
+ setError(null);
32
+
33
+ // Load user profile and karma total
34
+ Promise.all([
35
+ oxyServices.getUserById(userId),
36
+ oxyServices.getUserKarmaTotal ? oxyServices.getUserKarmaTotal(userId) : Promise.resolve({ total: undefined })
37
+ ])
38
+ .then(([profileRes, karmaRes]) => {
39
+ setProfile(profileRes);
40
+ setKarmaTotal(typeof karmaRes.total === 'number' ? karmaRes.total : null);
41
+
42
+ // Mock data for other stats
43
+ // In a real app, these would come from API endpoints
44
+ setPostsCount(Math.floor(Math.random() * 50));
45
+ setCommentsCount(Math.floor(Math.random() * 100));
46
+ setFollowersCount(Math.floor(Math.random() * 200));
47
+ setFollowingCount(Math.floor(Math.random() * 100));
48
+ })
49
+ .catch((err: any) => setError(err.message || 'Failed to load profile'))
50
+ .finally(() => setIsLoading(false));
51
+ }, [userId]);
52
+
53
+ if (isLoading) {
54
+ return (
55
+ <View style={[styles.container, { backgroundColor, justifyContent: 'center' }]}>
56
+ <ActivityIndicator size="large" color={primaryColor} />
57
+ </View>
58
+ );
59
+ }
60
+
61
+ if (error) {
62
+ return (
63
+ <View style={[styles.container, { backgroundColor, justifyContent: 'center' }]}>
64
+ <Text style={{ color: '#D32F2F', textAlign: 'center' }}>{error}</Text>
65
+ </View>
66
+ );
67
+ }
68
+
69
+ return (
70
+ <View style={[styles.container, { backgroundColor }]}>
71
+ <ScrollView contentContainerStyle={styles.scrollContainer}>
72
+ {/* Banner Image */}
73
+ <View style={styles.bannerContainer}>
74
+ <View style={styles.bannerImage} />
75
+ </View>
76
+ {/* Avatar overlapping banner */}
77
+ <View style={styles.avatarRow}>
78
+ <View style={styles.avatarWrapper}>
79
+ <Avatar imageUrl={profile?.avatarUrl} name={profile?.username || username} size={96} theme={theme} />
80
+ </View>
81
+ {/* Edit Profile/Follow Button placeholder */}
82
+ <View style={styles.actionButtonWrapper}>
83
+ <Text style={styles.actionButton}>Edit Profile</Text>
84
+ </View>
85
+ </View>
86
+ {/* Profile Info */}
87
+ <View style={styles.header}>
88
+ <Text style={[styles.displayName, { color: textColor }]}>{profile?.displayName || profile?.username || username || profile?.id}</Text>
89
+ {profile?.username && (
90
+ <Text style={[styles.subText, { color: isDarkTheme ? '#BBBBBB' : '#888888' }]}>@{profile.username}</Text>
91
+ )}
92
+ {/* Bio placeholder */}
93
+ <Text style={[styles.bio, { color: textColor }]}>{profile?.bio || 'This user has no bio yet.'}</Text>
94
+ {/* Email and Join Date Row */}
95
+ <View style={styles.infoRow}>
96
+ {profile?.email && (
97
+ <View style={styles.infoItem}>
98
+ <Ionicons name="mail-outline" size={16} color={isDarkTheme ? '#BBBBBB' : '#888888'} style={{ marginRight: 4 }} />
99
+ <Text style={[styles.infoText, { color: isDarkTheme ? '#BBBBBB' : '#888888' }]}>{profile.email}</Text>
100
+ </View>
101
+ )}
102
+ {profile?.createdAt && (
103
+ <View style={styles.infoItem}>
104
+ <Ionicons name="calendar-outline" size={16} color={isDarkTheme ? '#BBBBBB' : '#888888'} style={{ marginRight: 4 }} />
105
+ <Text style={[styles.infoText, { color: isDarkTheme ? '#BBBBBB' : '#888888' }]}>Joined {new Date(profile.createdAt).toLocaleDateString()}</Text>
106
+ </View>
107
+ )}
108
+ </View>
109
+ {/* Divider */}
110
+ <View style={styles.divider} />
111
+ {/* All Stats in one row */}
112
+ <View style={styles.statsRow}>
113
+ <View style={styles.statItem}>
114
+ <Text style={[styles.karmaAmount, { color: primaryColor }]}>{karmaTotal !== null && karmaTotal !== undefined ? karmaTotal : '--'}</Text>
115
+ <Text style={[styles.karmaLabel, { color: isDarkTheme ? '#BBBBBB' : '#888888' }]}>Karma</Text>
116
+ </View>
117
+ <View style={styles.statItem}>
118
+ <Text style={[styles.karmaAmount, { color: textColor }]}>{followersCount !== null ? followersCount : '--'}</Text>
119
+ <Text style={[styles.karmaLabel, { color: isDarkTheme ? '#BBBBBB' : '#888888' }]}>Followers</Text>
120
+ </View>
121
+ <View style={styles.statItem}>
122
+ <Text style={[styles.karmaAmount, { color: textColor }]}>{followingCount !== null ? followingCount : '--'}</Text>
123
+ <Text style={[styles.karmaLabel, { color: isDarkTheme ? '#BBBBBB' : '#888888' }]}>Following</Text>
124
+ </View>
125
+ </View>
126
+ </View>
127
+ </ScrollView>
128
+ </View>
129
+ );
130
+ };
131
+
132
+ const styles = StyleSheet.create({
133
+ container: { flex: 1 },
134
+ scrollContainer: { alignItems: 'stretch', paddingBottom: 40 },
135
+ bannerContainer: { height: 160, backgroundColor: '#e1bee7', position: 'relative', overflow: 'hidden' },
136
+ bannerImage: { flex: 1, backgroundColor: '#d169e5' }, // Placeholder, replace with Image if available
137
+ avatarRow: { flexDirection: 'row', alignItems: 'flex-end', marginTop: -56, paddingHorizontal: 20, justifyContent: 'space-between', zIndex: 2 },
138
+ avatarWrapper: { borderWidth: 5, borderColor: '#fff', borderRadius: 64, overflow: 'hidden', backgroundColor: '#fff', },
139
+ actionButtonWrapper: { flex: 1, alignItems: 'flex-end', justifyContent: 'flex-end' },
140
+ 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 },
141
+ header: { alignItems: 'flex-start', paddingTop: 18, paddingBottom: 24, width: '100%', paddingHorizontal: 20 },
142
+ displayName: { fontSize: 24, fontWeight: 'bold', marginTop: 10, marginBottom: 2, letterSpacing: 0.1 },
143
+ subText: { fontSize: 16, marginBottom: 2, color: '#a0a0a0' },
144
+ bio: { fontSize: 16, marginTop: 10, marginBottom: 10, color: '#666', fontStyle: 'italic', lineHeight: 22 },
145
+ infoRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 10, flexWrap: 'wrap' },
146
+ infoItem: { flexDirection: 'row', alignItems: 'center', marginRight: 28, marginBottom: 4, minWidth: 120 },
147
+ infoText: { fontSize: 15 },
148
+ divider: { height: 1, backgroundColor: '#e0e0e0', width: '100%', marginVertical: 14 },
149
+ statsRow: { width: '100%', flex: 1, flexDirection: 'row', alignItems: 'center', marginTop: 6, marginBottom: 2, justifyContent: 'space-between' },
150
+ statItem: { flex: 1, alignItems: 'center', minWidth: 50, marginBottom: 12 },
151
+ karmaLabel: { fontSize: 14, marginBottom: 2, textAlign: 'center', color: '#a0a0a0' },
152
+ karmaAmount: { fontSize: 24, fontWeight: 'bold', textAlign: 'center', letterSpacing: 0.2 },
153
+ });
154
+
155
+ export default ProfileScreen;
@@ -15,6 +15,7 @@ import { BaseScreenProps } from '../navigation/types';
15
15
  import { useOxy } from '../context/OxyContext';
16
16
  import { fontFamilies, useThemeColors, createCommonStyles } from '../styles';
17
17
  import OxyLogo from '../components/OxyLogo';
18
+ import { BottomSheetScrollView } from '@gorhom/bottom-sheet';
18
19
 
19
20
  const SignInScreen: React.FC<BaseScreenProps> = ({
20
21
  navigate,
@@ -77,92 +78,86 @@ const SignInScreen: React.FC<BaseScreenProps> = ({
77
78
  };
78
79
 
79
80
  return (
80
- <KeyboardAvoidingView
81
- behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
82
- style={[commonStyles.container]}
81
+ <BottomSheetScrollView
82
+ contentContainerStyle={commonStyles.scrollContainer}
83
+ keyboardShouldPersistTaps="handled"
83
84
  >
84
- <ScrollView
85
- contentContainerStyle={commonStyles.scrollContainer}
86
- keyboardShouldPersistTaps="handled"
87
- >
88
- <OxyLogo
89
- style={{ marginBottom: 24 }}
90
- width={50}
91
- height={50}
92
- />
93
- <Text style={[
94
- styles.title,
95
- { color: colors.text }
96
- ]}>Sign In</Text>
97
-
98
- {errorMessage ? (
99
- <View style={commonStyles.errorContainer}>
100
- <Text style={commonStyles.errorText}>{errorMessage}</Text>
101
- </View>
102
- ) : null}
103
-
104
- <View style={styles.formContainer}>
105
- <View style={styles.inputContainer}>
106
- <Text style={[styles.label, { color: colors.text }]}>Username</Text>
107
- <TextInput
108
- style={commonStyles.input}
109
- placeholder="Enter your username"
110
- placeholderTextColor={colors.placeholder}
111
- value={username}
112
- onChangeText={setUsername}
113
- autoCapitalize="none"
114
- testID="username-input"
115
- />
116
- </View>
117
-
118
- <View style={styles.inputContainer}>
119
- <Text style={[styles.label, { color: colors.text }]}>Password</Text>
120
- <TextInput
121
- style={commonStyles.input}
122
- placeholder="Enter your password"
123
- placeholderTextColor={colors.placeholder}
124
- value={password}
125
- onChangeText={setPassword}
126
- secureTextEntry
127
- testID="password-input"
128
- />
129
- </View>
85
+ <OxyLogo
86
+ style={{ marginBottom: 24 }}
87
+ width={50}
88
+ height={50}
89
+ />
90
+ <Text style={[
91
+ styles.title,
92
+ { color: colors.text }
93
+ ]}>Sign In</Text>
94
+
95
+ {errorMessage ? (
96
+ <View style={commonStyles.errorContainer}>
97
+ <Text style={commonStyles.errorText}>{errorMessage}</Text>
98
+ </View>
99
+ ) : null}
100
+
101
+ <View style={styles.formContainer}>
102
+ <View style={styles.inputContainer}>
103
+ <Text style={[styles.label, { color: colors.text }]}>Username</Text>
104
+ <TextInput
105
+ style={commonStyles.input}
106
+ placeholder="Enter your username"
107
+ placeholderTextColor={colors.placeholder}
108
+ value={username}
109
+ onChangeText={setUsername}
110
+ autoCapitalize="none"
111
+ testID="username-input"
112
+ />
113
+ </View>
130
114
 
131
- <TouchableOpacity
132
- style={[commonStyles.button, { opacity: isLoading ? 0.8 : 1 }]}
133
- onPress={handleLogin}
134
- disabled={isLoading}
135
- testID="login-button"
136
- >
137
- {isLoading ? (
138
- <ActivityIndicator color="#FFFFFF" size="small" />
139
- ) : (
140
- <Text style={commonStyles.buttonText}>Sign In</Text>
141
- )}
142
- </TouchableOpacity>
115
+ <View style={styles.inputContainer}>
116
+ <Text style={[styles.label, { color: colors.text }]}>Password</Text>
117
+ <TextInput
118
+ style={commonStyles.input}
119
+ placeholder="Enter your password"
120
+ placeholderTextColor={colors.placeholder}
121
+ value={password}
122
+ onChangeText={setPassword}
123
+ secureTextEntry
124
+ testID="password-input"
125
+ />
126
+ </View>
143
127
 
144
- <View style={styles.footerTextContainer}>
145
- <Text style={[styles.footerText, { color: colors.text }]}>
146
- Don't have an account?{' '}
147
- </Text>
148
- <TouchableOpacity onPress={() => navigate('SignUp')}>
149
- <Text style={[styles.linkText, { color: colors.primary }]}>Sign Up</Text>
150
- </TouchableOpacity>
151
- </View>
128
+ <TouchableOpacity
129
+ style={[commonStyles.button, { opacity: isLoading ? 0.8 : 1 }]}
130
+ onPress={handleLogin}
131
+ disabled={isLoading}
132
+ testID="login-button"
133
+ >
134
+ {isLoading ? (
135
+ <ActivityIndicator color="#FFFFFF" size="small" />
136
+ ) : (
137
+ <Text style={commonStyles.buttonText}>Sign In</Text>
138
+ )}
139
+ </TouchableOpacity>
140
+
141
+ <View style={styles.footerTextContainer}>
142
+ <Text style={[styles.footerText, { color: colors.text }]}>
143
+ Don't have an account?{' '}
144
+ </Text>
145
+ <TouchableOpacity onPress={() => navigate('SignUp')}>
146
+ <Text style={[styles.linkText, { color: colors.primary }]}>Sign Up</Text>
147
+ </TouchableOpacity>
152
148
  </View>
153
- </ScrollView>
154
- </KeyboardAvoidingView>
149
+ </View>
150
+ </BottomSheetScrollView>
155
151
  );
156
152
  };
157
153
 
158
154
  const styles = StyleSheet.create({
159
- // Local screen-specific styles
160
155
  title: {
161
156
  fontFamily: Platform.OS === 'web'
162
157
  ? 'Phudu' // Use CSS font name directly for web
163
158
  : 'Phudu-Bold', // Use exact font name as registered with Font.loadAsync
164
159
  fontWeight: Platform.OS === 'web' ? 'bold' : undefined, // Only apply fontWeight on web
165
- fontSize: 44,
160
+ fontSize: 54,
166
161
  marginBottom: 24,
167
162
  },
168
163
  formContainer: {