@oxyhq/services 6.9.11 → 6.9.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 (117) hide show
  1. package/lib/commonjs/ui/components/FollowButton.js +135 -118
  2. package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
  3. package/lib/commonjs/ui/components/GroupedSection.js.map +1 -1
  4. package/lib/commonjs/ui/components/fileManagement/FileDetailsModal.js.map +1 -1
  5. package/lib/commonjs/ui/components/fileManagement/UploadPreview.js.map +1 -1
  6. package/lib/commonjs/ui/components/internal/GroupedPillButtons.js.map +1 -1
  7. package/lib/commonjs/ui/components/payment/PaymentReviewStep.js.map +1 -1
  8. package/lib/commonjs/ui/components/payment/PaymentSummaryStep.js.map +1 -1
  9. package/lib/commonjs/ui/context/hooks/useAuthOperations.js +6 -2
  10. package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
  11. package/lib/commonjs/ui/hooks/mutations/useServicesMutations.js.map +1 -1
  12. package/lib/commonjs/ui/hooks/useAuth.js +9 -3
  13. package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
  14. package/lib/commonjs/ui/hooks/useFollow.js +134 -74
  15. package/lib/commonjs/ui/hooks/useFollow.js.map +1 -1
  16. package/lib/commonjs/ui/hooks/useWebSSO.js.map +1 -1
  17. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js +30 -11
  18. package/lib/commonjs/ui/screens/EditProfileFieldScreen.js.map +1 -1
  19. package/lib/commonjs/ui/screens/FAQScreen.js +1 -0
  20. package/lib/commonjs/ui/screens/FAQScreen.js.map +1 -1
  21. package/lib/commonjs/ui/screens/FeedbackScreen.js.map +1 -1
  22. package/lib/commonjs/ui/screens/FileManagementScreen.js +0 -1
  23. package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
  24. package/lib/commonjs/ui/screens/HistoryViewScreen.js.map +1 -1
  25. package/lib/commonjs/ui/screens/PremiumSubscriptionScreen.js.map +1 -1
  26. package/lib/commonjs/ui/screens/karma/KarmaRewardsScreen.js.map +1 -1
  27. package/lib/commonjs/ui/stores/fileStore.js +6 -6
  28. package/lib/commonjs/ui/stores/fileStore.js.map +1 -1
  29. package/lib/commonjs/ui/utils/fileManagement.js +6 -3
  30. package/lib/commonjs/ui/utils/fileManagement.js.map +1 -1
  31. package/lib/module/ui/components/FollowButton.js +137 -121
  32. package/lib/module/ui/components/FollowButton.js.map +1 -1
  33. package/lib/module/ui/components/GroupedSection.js.map +1 -1
  34. package/lib/module/ui/components/fileManagement/FileDetailsModal.js.map +1 -1
  35. package/lib/module/ui/components/fileManagement/UploadPreview.js.map +1 -1
  36. package/lib/module/ui/components/internal/GroupedPillButtons.js.map +1 -1
  37. package/lib/module/ui/components/payment/PaymentReviewStep.js.map +1 -1
  38. package/lib/module/ui/components/payment/PaymentSummaryStep.js.map +1 -1
  39. package/lib/module/ui/context/hooks/useAuthOperations.js +7 -2
  40. package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
  41. package/lib/module/ui/hooks/mutations/useServicesMutations.js.map +1 -1
  42. package/lib/module/ui/hooks/useAuth.js +9 -3
  43. package/lib/module/ui/hooks/useAuth.js.map +1 -1
  44. package/lib/module/ui/hooks/useFollow.js +132 -72
  45. package/lib/module/ui/hooks/useFollow.js.map +1 -1
  46. package/lib/module/ui/hooks/useWebSSO.js.map +1 -1
  47. package/lib/module/ui/screens/EditProfileFieldScreen.js +30 -11
  48. package/lib/module/ui/screens/EditProfileFieldScreen.js.map +1 -1
  49. package/lib/module/ui/screens/FAQScreen.js +1 -0
  50. package/lib/module/ui/screens/FAQScreen.js.map +1 -1
  51. package/lib/module/ui/screens/FeedbackScreen.js.map +1 -1
  52. package/lib/module/ui/screens/FileManagementScreen.js +0 -1
  53. package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
  54. package/lib/module/ui/screens/HistoryViewScreen.js.map +1 -1
  55. package/lib/module/ui/screens/PremiumSubscriptionScreen.js.map +1 -1
  56. package/lib/module/ui/screens/karma/KarmaRewardsScreen.js.map +1 -1
  57. package/lib/module/ui/stores/fileStore.js +6 -6
  58. package/lib/module/ui/stores/fileStore.js.map +1 -1
  59. package/lib/module/ui/utils/fileManagement.js +6 -3
  60. package/lib/module/ui/utils/fileManagement.js.map +1 -1
  61. package/lib/typescript/commonjs/ui/components/FollowButton.d.ts +12 -1
  62. package/lib/typescript/commonjs/ui/components/FollowButton.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/ui/components/GroupedSection.d.ts +5 -0
  64. package/lib/typescript/commonjs/ui/components/GroupedSection.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/ui/components/feedback/types.d.ts +2 -2
  66. package/lib/typescript/commonjs/ui/components/feedback/types.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/ui/components/internal/GroupedPillButtons.d.ts +7 -1
  68. package/lib/typescript/commonjs/ui/components/internal/GroupedPillButtons.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts +31 -0
  72. package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/ui/screens/FAQScreen.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/ui/screens/HistoryViewScreen.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/ui/utils/fileManagement.d.ts.map +1 -1
  78. package/lib/typescript/module/ui/components/FollowButton.d.ts +12 -1
  79. package/lib/typescript/module/ui/components/FollowButton.d.ts.map +1 -1
  80. package/lib/typescript/module/ui/components/GroupedSection.d.ts +5 -0
  81. package/lib/typescript/module/ui/components/GroupedSection.d.ts.map +1 -1
  82. package/lib/typescript/module/ui/components/feedback/types.d.ts +2 -2
  83. package/lib/typescript/module/ui/components/feedback/types.d.ts.map +1 -1
  84. package/lib/typescript/module/ui/components/internal/GroupedPillButtons.d.ts +7 -1
  85. package/lib/typescript/module/ui/components/internal/GroupedPillButtons.d.ts.map +1 -1
  86. package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
  87. package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
  88. package/lib/typescript/module/ui/hooks/useFollow.d.ts +31 -0
  89. package/lib/typescript/module/ui/hooks/useFollow.d.ts.map +1 -1
  90. package/lib/typescript/module/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
  91. package/lib/typescript/module/ui/screens/FAQScreen.d.ts.map +1 -1
  92. package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
  93. package/lib/typescript/module/ui/screens/HistoryViewScreen.d.ts.map +1 -1
  94. package/lib/typescript/module/ui/utils/fileManagement.d.ts.map +1 -1
  95. package/package.json +2 -2
  96. package/src/ui/components/FollowButton.tsx +117 -109
  97. package/src/ui/components/GroupedSection.tsx +5 -0
  98. package/src/ui/components/feedback/types.ts +2 -2
  99. package/src/ui/components/fileManagement/FileDetailsModal.tsx +1 -1
  100. package/src/ui/components/fileManagement/UploadPreview.tsx +1 -1
  101. package/src/ui/components/internal/GroupedPillButtons.tsx +17 -8
  102. package/src/ui/components/payment/PaymentReviewStep.tsx +1 -1
  103. package/src/ui/components/payment/PaymentSummaryStep.tsx +1 -1
  104. package/src/ui/context/hooks/useAuthOperations.ts +8 -3
  105. package/src/ui/hooks/mutations/useServicesMutations.ts +3 -3
  106. package/src/ui/hooks/useAuth.ts +10 -4
  107. package/src/ui/hooks/useFollow.ts +161 -74
  108. package/src/ui/hooks/useWebSSO.ts +3 -3
  109. package/src/ui/screens/EditProfileFieldScreen.tsx +28 -16
  110. package/src/ui/screens/FAQScreen.tsx +2 -1
  111. package/src/ui/screens/FeedbackScreen.tsx +7 -7
  112. package/src/ui/screens/FileManagementScreen.tsx +1 -2
  113. package/src/ui/screens/HistoryViewScreen.tsx +5 -1
  114. package/src/ui/screens/PremiumSubscriptionScreen.tsx +1 -1
  115. package/src/ui/screens/karma/KarmaRewardsScreen.tsx +1 -1
  116. package/src/ui/stores/fileStore.ts +9 -9
  117. package/src/ui/utils/fileManagement.ts +16 -10
@@ -1,5 +1,4 @@
1
- import type React from 'react';
2
- import { useEffect, useCallback } from 'react';
1
+ import React, { useEffect, useCallback, memo } from 'react';
3
2
  import {
4
3
  TouchableOpacity,
5
4
  Text,
@@ -21,8 +20,9 @@ import Animated, {
21
20
  import { useOxy } from '../context/OxyContext';
22
21
  import { fontFamilies } from '../styles/fonts';
23
22
  import { toast } from '../../lib/sonner';
24
- import { useFollow } from '../hooks/useFollow';
23
+ import { useFollowForButton } from '../hooks/useFollow';
25
24
  import { useThemeColors } from '../styles/theme';
25
+ import type { OxyServices } from '@oxyhq/core';
26
26
 
27
27
  // Create animated TouchableOpacity
28
28
  const AnimatedTouchableOpacity = Animated.createAnimatedComponent(TouchableOpacity);
@@ -41,8 +41,22 @@ export interface FollowButtonProps {
41
41
  theme?: 'light' | 'dark';
42
42
  }
43
43
 
44
- const FollowButton: React.FC<FollowButtonProps> = ({
44
+ /**
45
+ * Inner component that handles all hooks and rendering.
46
+ *
47
+ * Separated from the outer wrapper to avoid a Rules of Hooks violation.
48
+ * The outer wrapper handles the auth/self-follow guard and returns null
49
+ * before any hooks are called. This inner component always renders
50
+ * (all hooks are called unconditionally).
51
+ *
52
+ * Receives oxyServices as a prop instead of calling useOxy(), so it does
53
+ * not subscribe to the OxyContext. This is critical in list contexts where
54
+ * N buttons would all re-render on any context change (session socket events,
55
+ * token refreshes, etc.).
56
+ */
57
+ const FollowButtonInner = memo(function FollowButtonInner({
45
58
  userId,
59
+ oxyServices,
46
60
  initiallyFollowing = false,
47
61
  size = 'medium',
48
62
  onFollowChange,
@@ -52,42 +66,23 @@ const FollowButton: React.FC<FollowButtonProps> = ({
52
66
  showLoadingState = true,
53
67
  preventParentActions = true,
54
68
  theme = 'light',
55
- }) => {
56
- const { oxyServices, isAuthenticated, user: currentUser } = useOxy();
69
+ }: FollowButtonProps & { oxyServices: OxyServices }) {
57
70
  const colors = useThemeColors(theme);
58
71
 
59
- // Safety check: Don't render follow button on own profile
60
- // This provides a fallback in case parent components don't handle this check
61
- // Normalize IDs by trimming whitespace and comparing as strings
62
- const normalizeId = (id: string | undefined | null): string => {
63
- if (!id) return '';
64
- return String(id).trim();
65
- };
66
-
67
- const currentUserId = normalizeId(currentUser?.id);
68
- const targetUserId = normalizeId(userId);
69
-
70
- // Don't render if:
71
- // 1. Not authenticated (can't follow anyway)
72
- // 2. Viewing own profile (currentUser.id matches userId)
73
- if (!isAuthenticated || (currentUserId && targetUserId && currentUserId === targetUserId)) {
74
- return null;
75
- }
72
+ // Uses granular Zustand selectors only re-renders when THIS user's data changes
76
73
  const {
77
74
  isFollowing,
78
75
  isLoading,
79
- error,
80
76
  toggleFollow,
81
77
  setFollowStatus,
82
78
  fetchStatus,
83
- clearError,
84
- } = useFollow(userId);
79
+ } = useFollowForButton(userId, oxyServices);
85
80
 
86
81
  // Animation values
87
82
  const animationProgress = useSharedValue(isFollowing ? 1 : 0);
88
83
  const scale = useSharedValue(1);
89
84
 
90
- // Button press handler with animation
85
+ // Stable press handler depends on primitives only
91
86
  const handlePress = useCallback(async (event?: { preventDefault?: () => void; stopPropagation?: () => void }) => {
92
87
  if (preventParentActions && event && event.preventDefault) {
93
88
  event.preventDefault();
@@ -103,7 +98,7 @@ const FollowButton: React.FC<FollowButtonProps> = ({
103
98
  });
104
99
 
105
100
  try {
106
- await toggleFollow?.();
101
+ await toggleFollow();
107
102
  if (onFollowChange) onFollowChange(!isFollowing);
108
103
  } catch (err: unknown) {
109
104
  const error = err instanceof Error ? err : new Error(String(err));
@@ -111,26 +106,28 @@ const FollowButton: React.FC<FollowButtonProps> = ({
111
106
  }
112
107
  }, [disabled, isLoading, toggleFollow, onFollowChange, isFollowing, preventParentActions, scale]);
113
108
 
114
- // biome-ignore lint/correctness/useExhaustiveDependencies: intentional mount-only initialization
109
+ // Set initial follow status on mount if provided and not already set
115
110
  useEffect(() => {
116
111
  if (userId && !isFollowing && initiallyFollowing) {
117
- setFollowStatus?.(initiallyFollowing);
112
+ setFollowStatus(initiallyFollowing);
118
113
  }
114
+ // Intentional: only run on mount with initial values
115
+ // eslint-disable-next-line react-hooks/exhaustive-deps
119
116
  }, [userId, initiallyFollowing]);
120
117
 
121
- // Fetch latest follow status from backend on mount if authenticated
118
+ // Fetch latest follow status from backend on mount
122
119
  useEffect(() => {
123
- if (userId && isAuthenticated) {
124
- fetchStatus?.();
120
+ if (userId) {
121
+ fetchStatus();
125
122
  }
126
- }, [userId, fetchStatus, isAuthenticated]);
123
+ }, [userId, fetchStatus]);
127
124
 
128
125
  // Animate button on follow/unfollow
129
126
  useEffect(() => {
130
127
  animationProgress.value = withTiming(isFollowing ? 1 : 0, { duration: 300, easing: Easing.inOut(Easing.ease) });
131
128
  }, [isFollowing, animationProgress]);
132
129
 
133
- // Animated styles for better performance
130
+ // Animated styles
134
131
  const animatedButtonStyle = useAnimatedStyle(() => {
135
132
  return {
136
133
  transform: [{ scale: scale.value }],
@@ -157,98 +154,109 @@ const FollowButton: React.FC<FollowButtonProps> = ({
157
154
  };
158
155
  }, [colors]);
159
156
 
160
- // Get base button style (without state-specific colors since they're animated)
161
- const getBaseButtonStyle = (): StyleProp<ViewStyle> => {
162
- const baseStyle = {
163
- flexDirection: 'row' as const,
164
- alignItems: 'center' as const,
165
- justifyContent: 'center' as const,
166
- borderWidth: 1,
167
- ...Platform.select({
168
- web: {
169
- boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
170
- },
171
- default: {
172
- shadowColor: '#000',
173
- shadowOffset: { width: 0, height: 2 },
174
- shadowOpacity: 0.1,
175
- shadowRadius: 4,
176
- elevation: 2,
177
- }
178
- }),
179
- };
180
-
181
- // Size-specific styles
182
- let sizeStyle = {};
183
- if (size === 'small') {
184
- sizeStyle = {
185
- paddingVertical: 6,
186
- paddingHorizontal: 12,
187
- minWidth: 70,
188
- borderRadius: 35,
189
- };
190
- } else if (size === 'large') {
191
- sizeStyle = {
192
- paddingVertical: 12,
193
- paddingHorizontal: 24,
194
- minWidth: 120,
195
- borderRadius: 35,
196
- };
197
- } else {
198
- // medium
199
- sizeStyle = {
200
- paddingVertical: 8,
201
- paddingHorizontal: 16,
202
- minWidth: 90,
203
- borderRadius: 35,
204
- };
205
- }
206
-
207
- return [baseStyle, sizeStyle, style];
208
- };
209
-
210
- // Get base text style (without state-specific colors since they're animated)
211
- const getBaseTextStyle = (): StyleProp<TextStyle> => {
212
- const baseTextStyle = {
213
- fontFamily: fontFamilies.interSemiBold,
214
- fontWeight: '600' as const,
215
- };
216
-
217
- // Size-specific text styles
218
- let sizeTextStyle = {};
219
- if (size === 'small') {
220
- sizeTextStyle = { fontSize: 13 };
221
- } else if (size === 'large') {
222
- sizeTextStyle = { fontSize: 16 };
223
- } else {
224
- // medium
225
- sizeTextStyle = { fontSize: 15 };
226
- }
227
-
228
- return [baseTextStyle, sizeTextStyle, textStyle];
229
- };
157
+ const baseButtonStyle = getBaseButtonStyle(size, style);
158
+ const baseTextStyle = getBaseTextStyle(size, textStyle);
230
159
 
231
160
  return (
232
161
  <AnimatedTouchableOpacity
233
- style={[getBaseButtonStyle(), animatedButtonStyle]}
162
+ style={[baseButtonStyle, animatedButtonStyle]}
234
163
  onPress={handlePress}
235
164
  disabled={disabled || isLoading}
236
165
  activeOpacity={0.8}
237
166
  >
238
167
  {showLoadingState && isLoading ? (
239
168
  <ActivityIndicator
240
- size={size === 'small' ? 'small' : 'small'}
169
+ size="small"
241
170
  color={isFollowing ? '#FFFFFF' : colors.primary}
242
171
  />
243
172
  ) : (
244
- <AnimatedText style={[getBaseTextStyle(), animatedTextStyle]}>
173
+ <AnimatedText style={[baseTextStyle, animatedTextStyle]}>
245
174
  {isFollowing ? 'Following' : 'Follow'}
246
175
  </AnimatedText>
247
176
  )}
248
177
  </AnimatedTouchableOpacity>
249
178
  );
179
+ });
180
+
181
+ /**
182
+ * Outer wrapper that handles the "should we render?" check.
183
+ *
184
+ * This is the ONLY place useOxy() is called — to check authentication and
185
+ * get the current user ID for the self-follow guard. The oxyServices instance
186
+ * is passed down as a prop to the inner component, which avoids subscribing
187
+ * to the full OxyContext.
188
+ *
189
+ * The early return happens BEFORE the inner component mounts, so the inner
190
+ * component's hooks are never called conditionally (no Rules of Hooks violation).
191
+ */
192
+ const FollowButton: React.FC<FollowButtonProps> = (props) => {
193
+ const { oxyServices, isAuthenticated, user: currentUser } = useOxy();
194
+
195
+ const currentUserId = currentUser?.id ? String(currentUser.id).trim() : '';
196
+ const targetUserId = props.userId ? String(props.userId).trim() : '';
197
+
198
+ // Don't render if not authenticated or viewing own profile
199
+ if (!isAuthenticated || !targetUserId || (currentUserId && currentUserId === targetUserId)) {
200
+ return null;
201
+ }
202
+
203
+ return (
204
+ <FollowButtonInner
205
+ {...props}
206
+ userId={targetUserId}
207
+ oxyServices={oxyServices}
208
+ />
209
+ );
250
210
  };
251
211
 
212
+ // Pure helper functions (no hooks, no state) extracted outside the component
213
+ function getBaseButtonStyle(size: string, style?: StyleProp<ViewStyle>): StyleProp<ViewStyle> {
214
+ const baseStyle: ViewStyle = {
215
+ flexDirection: 'row',
216
+ alignItems: 'center',
217
+ justifyContent: 'center',
218
+ borderWidth: 1,
219
+ ...Platform.select({
220
+ web: {},
221
+ default: {
222
+ shadowColor: '#000',
223
+ shadowOffset: { width: 0, height: 2 },
224
+ shadowOpacity: 0.1,
225
+ shadowRadius: 4,
226
+ elevation: 2,
227
+ }
228
+ }),
229
+ };
230
+
231
+ let sizeStyle: ViewStyle;
232
+ if (size === 'small') {
233
+ sizeStyle = { paddingVertical: 6, paddingHorizontal: 12, minWidth: 70, borderRadius: 35 };
234
+ } else if (size === 'large') {
235
+ sizeStyle = { paddingVertical: 12, paddingHorizontal: 24, minWidth: 120, borderRadius: 35 };
236
+ } else {
237
+ sizeStyle = { paddingVertical: 8, paddingHorizontal: 16, minWidth: 90, borderRadius: 35 };
238
+ }
239
+
240
+ return [baseStyle, sizeStyle, style];
241
+ }
242
+
243
+ function getBaseTextStyle(size: string, textStyle?: StyleProp<TextStyle>): StyleProp<TextStyle> {
244
+ const baseTextStyle: TextStyle = {
245
+ fontFamily: fontFamilies.interSemiBold,
246
+ fontWeight: '600',
247
+ };
248
+
249
+ let sizeTextStyle: TextStyle;
250
+ if (size === 'small') {
251
+ sizeTextStyle = { fontSize: 13 };
252
+ } else if (size === 'large') {
253
+ sizeTextStyle = { fontSize: 16 };
254
+ } else {
255
+ sizeTextStyle = { fontSize: 15 };
256
+ }
257
+
258
+ return [baseTextStyle, sizeTextStyle, textStyle];
259
+ }
252
260
 
253
261
  export { FollowButton };
254
- export default FollowButton;
262
+ export default FollowButton;
@@ -11,13 +11,18 @@ interface GroupedSectionItem {
11
11
  title: string;
12
12
  subtitle?: string;
13
13
  onPress?: () => void;
14
+ onLongPress?: () => void;
14
15
  showChevron?: boolean;
15
16
  disabled?: boolean;
17
+ selected?: boolean;
16
18
  customContent?: React.ReactNode;
17
19
  customIcon?: React.ReactNode;
18
20
  multiRow?: boolean;
19
21
  dense?: boolean;
20
22
  customContentBelow?: React.ReactNode;
23
+ theme?: string;
24
+ /** Allow additional properties for extensibility */
25
+ [key: string]: unknown;
21
26
  }
22
27
 
23
28
  interface GroupedSectionProps {
@@ -16,7 +16,7 @@ export interface FeedbackState {
16
16
  }
17
17
 
18
18
  export interface FeedbackType {
19
- id: string;
19
+ id: FeedbackData['type'];
20
20
  label: string;
21
21
  icon: string;
22
22
  color: string;
@@ -24,7 +24,7 @@ export interface FeedbackType {
24
24
  }
25
25
 
26
26
  export interface PriorityLevel {
27
- id: string;
27
+ id: FeedbackData['priority'];
28
28
  label: string;
29
29
  icon: string;
30
30
  color: string;
@@ -53,7 +53,7 @@ export const FileDetailsModal: React.FC<FileDetailsModalProps> = ({
53
53
  <View style={[fileManagementStyles.fileDetailCard, { backgroundColor: themeStyles.secondaryBackgroundColor, borderColor }]}>
54
54
  <View style={fileManagementStyles.fileDetailIcon}>
55
55
  <Ionicons
56
- name={getFileIcon(file.contentType) as any}
56
+ name={getFileIcon(file.contentType) as React.ComponentProps<typeof Ionicons>['name']}
57
57
  size={64}
58
58
  color={themeStyles.primaryColor}
59
59
  />
@@ -72,7 +72,7 @@ const UploadPreviewContent: React.FC<{
72
72
  ) : (
73
73
  <View style={[fileManagementStyles.uploadPreviewIconContainer, { backgroundColor: themeStyles.isDarkTheme ? '#333333' : '#F0F0F0' }]}>
74
74
  <Ionicons
75
- name={getFileIcon(pendingFile.type) as any}
75
+ name={getFileIcon(pendingFile.type) as React.ComponentProps<typeof Ionicons>['name']}
76
76
  size={32}
77
77
  color={themeStyles.primaryColor}
78
78
  />
@@ -2,6 +2,8 @@ import type React from 'react';
2
2
  import { View, TouchableOpacity, Text, ActivityIndicator, StyleSheet, Platform } from 'react-native';
3
3
  import { Ionicons } from '@expo/vector-icons';
4
4
 
5
+ type IoniconsName = React.ComponentProps<typeof Ionicons>['name'];
6
+
5
7
  interface ButtonConfig {
6
8
  text: string;
7
9
  onPress: () => void;
@@ -12,9 +14,16 @@ interface ButtonConfig {
12
14
  testID?: string;
13
15
  }
14
16
 
17
+ interface GroupedPillButtonColors {
18
+ primary: string;
19
+ secondary?: string;
20
+ border: string;
21
+ text: string;
22
+ }
23
+
15
24
  interface GroupedPillButtonsProps {
16
25
  buttons: ButtonConfig[];
17
- colors: any;
26
+ colors: GroupedPillButtonColors;
18
27
  gap?: number;
19
28
  }
20
29
 
@@ -100,7 +109,7 @@ const GroupedPillButtons: React.FC<GroupedPillButtonsProps> = ({
100
109
  };
101
110
  };
102
111
 
103
- const getTextStyle = (button: ButtonConfig, colors: any) => {
112
+ const getTextStyle = (button: ButtonConfig, colors: GroupedPillButtonColors) => {
104
113
  const baseTextStyle = {
105
114
  fontSize: 15,
106
115
  fontWeight: '600' as const,
@@ -124,11 +133,11 @@ const GroupedPillButtons: React.FC<GroupedPillButtonsProps> = ({
124
133
  return {
125
134
  ...baseTextStyle,
126
135
  color: textColor,
127
- ...(Platform.OS === 'web' ? { whiteSpace: 'nowrap' as any } : null),
136
+ ...(Platform.OS === 'web' ? { whiteSpace: 'nowrap' as const } : null),
128
137
  };
129
138
  };
130
139
 
131
- const getIconColor = (button: ButtonConfig, colors: any) => {
140
+ const getIconColor = (button: ButtonConfig, colors: GroupedPillButtonColors) => {
132
141
  const isDisabled = button.disabled || button.loading;
133
142
 
134
143
  switch (button.variant) {
@@ -147,7 +156,7 @@ const GroupedPillButtons: React.FC<GroupedPillButtonsProps> = ({
147
156
  button.icon === 'chevron-back';
148
157
  };
149
158
 
150
- const renderButtonContent = (button: ButtonConfig, colors: any, index: number) => {
159
+ const renderButtonContent = (button: ButtonConfig, colors: GroupedPillButtonColors, index: number) => {
151
160
  const iconColor = getIconColor(button, colors);
152
161
  const isBack = isBackButton(button);
153
162
  const isFirstButton = index === 0;
@@ -172,7 +181,7 @@ const GroupedPillButtons: React.FC<GroupedPillButtonsProps> = ({
172
181
  </Text>
173
182
  {button.icon && (
174
183
  <Ionicons
175
- name={button.icon as any}
184
+ name={button.icon as IoniconsName}
176
185
  size={16}
177
186
  color={iconColor}
178
187
  />
@@ -185,7 +194,7 @@ const GroupedPillButtons: React.FC<GroupedPillButtonsProps> = ({
185
194
  <>
186
195
  {button.icon && (
187
196
  <Ionicons
188
- name={button.icon as any}
197
+ name={button.icon as IoniconsName}
189
198
  size={16}
190
199
  color={iconColor}
191
200
  />
@@ -204,7 +213,7 @@ const GroupedPillButtons: React.FC<GroupedPillButtonsProps> = ({
204
213
  </Text>
205
214
  {button.icon && (
206
215
  <Ionicons
207
- name={button.icon as any}
216
+ name={button.icon as IoniconsName}
208
217
  size={16}
209
218
  color={iconColor}
210
219
  />
@@ -74,7 +74,7 @@ const PaymentReviewStep: React.FC<PaymentReviewStepProps> = ({
74
74
  },
75
75
  {
76
76
  id: 'payment-method',
77
- icon: selectedMethod?.icon as any,
77
+ icon: selectedMethod?.icon,
78
78
  iconColor: colors.primary,
79
79
  title: t('payment.review.paymentMethod'),
80
80
  subtitle: selectedMethod ? t(`payment.methods.${selectedMethod.key}.label`) : undefined,
@@ -84,7 +84,7 @@ const PaymentSummaryStep: React.FC<PaymentSummaryStepProps> = ({
84
84
  <GroupedSection
85
85
  items={paymentItems.map((item, idx) => ({
86
86
  id: `item-${idx}`,
87
- icon: getItemTypeIcon(item.type) as any,
87
+ icon: getItemTypeIcon(item.type),
88
88
  iconColor: colors.primary,
89
89
  title: `${item.type === 'product' && item.quantity ? `${item.quantity} × ` : ''}${item.name}${item.type === 'subscription' && item.period ? ` (${item.period})` : ''}`,
90
90
  subtitle: item.description || `${item.currency ? (CURRENCY_SYMBOLS[item.currency.toUpperCase()] || item.currency) : currencySymbol} ${item.price * (item.quantity ?? 1)}`,
@@ -10,6 +10,11 @@ import type { OxyServices } from '@oxyhq/core';
10
10
  import { SignatureService } from '@oxyhq/core';
11
11
  import * as Crypto from 'expo-crypto';
12
12
 
13
+ /** Type guard for error objects with optional code and status properties */
14
+ function isErrorWithCodeOrStatus(error: unknown): error is { code?: string; status?: number; message?: string } {
15
+ return typeof error === 'object' && error !== null;
16
+ }
17
+
13
18
  export interface UseAuthOperationsOptions {
14
19
  oxyServices: OxyServices;
15
20
  storage: StorageInterface | null;
@@ -95,8 +100,8 @@ export const useAuthOperations = ({
95
100
  errorMessage.includes('network') ||
96
101
  errorMessage.includes('Failed to fetch') ||
97
102
  errorMessage.includes('fetch failed') ||
98
- (error as any)?.code === 'NETWORK_ERROR' ||
99
- (error as any)?.status === 0;
103
+ (isErrorWithCodeOrStatus(error) && error.code === 'NETWORK_ERROR') ||
104
+ (isErrorWithCodeOrStatus(error) && error.status === 0);
100
105
 
101
106
  if (isNetworkError) {
102
107
  if (__DEV__ && logger) {
@@ -189,7 +194,7 @@ export const useAuthOperations = ({
189
194
  await oxyServices.getTokenBySession(sessionResponse.sessionId);
190
195
  } catch (tokenError: unknown) {
191
196
  const errorMessage = tokenError instanceof Error ? tokenError.message : String(tokenError);
192
- const status = (tokenError as any)?.status;
197
+ const status = isErrorWithCodeOrStatus(tokenError) ? tokenError.status : undefined;
193
198
  if (status === 404 || errorMessage.includes('404')) {
194
199
  throw new Error(`Session was created but token could not be retrieved. Session ID: ${sessionResponse.sessionId.substring(0, 8)}...`);
195
200
  }
@@ -1,5 +1,5 @@
1
1
  import { useMutation, useQueryClient } from '@tanstack/react-query';
2
- import type { User } from '@oxyhq/core';
2
+ import type { User, ClientSession } from '@oxyhq/core';
3
3
  import { queryKeys, invalidateSessionQueries } from '../queries/queryKeys';
4
4
  import { useOxy } from '../../context/OxyContext';
5
5
  import { toast } from '../../../lib/sonner';
@@ -59,8 +59,8 @@ export const useLogoutSession = () => {
59
59
  // Optimistically remove session
60
60
  if (previousSessions) {
61
61
  const sessionToLogout = targetSessionId || activeSessionId;
62
- const updatedSessions = (previousSessions as any[]).filter(
63
- (s: any) => s.sessionId !== sessionToLogout
62
+ const updatedSessions = (previousSessions as ClientSession[]).filter(
63
+ (s) => s.sessionId !== sessionToLogout
64
64
  );
65
65
  queryClient.setQueryData(queryKeys.sessions.list(), updatedSessions);
66
66
  }
@@ -118,11 +118,17 @@ export function useAuth(): UseAuthReturn {
118
118
  // If user is clicking "Sign In", they need interactive auth NOW
119
119
  if (isWebBrowser() && !publicKey && !isIdentityProvider) {
120
120
  try {
121
- const popupSession = await (oxyServices as any).signInWithPopup?.();
121
+ const popupSession = await oxyServices.signInWithPopup?.();
122
122
  if (popupSession?.user) {
123
- // Update context state with the session (this updates user, sessions, storage)
124
- await handlePopupSession(popupSession);
125
- return popupSession.user;
123
+ // The popup auth flow fetches full user data, so the session user
124
+ // contains full User fields even though the base type is MinimalUserData.
125
+ // Cast to the expected shape for handlePopupSession.
126
+ const sessionWithUser = {
127
+ ...popupSession,
128
+ user: popupSession.user as unknown as User,
129
+ };
130
+ await handlePopupSession(sessionWithUser);
131
+ return sessionWithUser.user;
126
132
  }
127
133
  throw new Error('Sign-in failed. Please try again.');
128
134
  } catch (popupError) {