@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.
- package/lib/commonjs/ui/components/FollowButton.js +135 -118
- package/lib/commonjs/ui/components/FollowButton.js.map +1 -1
- package/lib/commonjs/ui/components/GroupedSection.js.map +1 -1
- package/lib/commonjs/ui/components/fileManagement/FileDetailsModal.js.map +1 -1
- package/lib/commonjs/ui/components/fileManagement/UploadPreview.js.map +1 -1
- package/lib/commonjs/ui/components/internal/GroupedPillButtons.js.map +1 -1
- package/lib/commonjs/ui/components/payment/PaymentReviewStep.js.map +1 -1
- package/lib/commonjs/ui/components/payment/PaymentSummaryStep.js.map +1 -1
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js +6 -2
- package/lib/commonjs/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/commonjs/ui/hooks/mutations/useServicesMutations.js.map +1 -1
- package/lib/commonjs/ui/hooks/useAuth.js +9 -3
- package/lib/commonjs/ui/hooks/useAuth.js.map +1 -1
- package/lib/commonjs/ui/hooks/useFollow.js +134 -74
- package/lib/commonjs/ui/hooks/useFollow.js.map +1 -1
- package/lib/commonjs/ui/hooks/useWebSSO.js.map +1 -1
- package/lib/commonjs/ui/screens/EditProfileFieldScreen.js +30 -11
- package/lib/commonjs/ui/screens/EditProfileFieldScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/FAQScreen.js +1 -0
- package/lib/commonjs/ui/screens/FAQScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/FeedbackScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/FileManagementScreen.js +0 -1
- package/lib/commonjs/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/HistoryViewScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/PremiumSubscriptionScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/karma/KarmaRewardsScreen.js.map +1 -1
- package/lib/commonjs/ui/stores/fileStore.js +6 -6
- package/lib/commonjs/ui/stores/fileStore.js.map +1 -1
- package/lib/commonjs/ui/utils/fileManagement.js +6 -3
- package/lib/commonjs/ui/utils/fileManagement.js.map +1 -1
- package/lib/module/ui/components/FollowButton.js +137 -121
- package/lib/module/ui/components/FollowButton.js.map +1 -1
- package/lib/module/ui/components/GroupedSection.js.map +1 -1
- package/lib/module/ui/components/fileManagement/FileDetailsModal.js.map +1 -1
- package/lib/module/ui/components/fileManagement/UploadPreview.js.map +1 -1
- package/lib/module/ui/components/internal/GroupedPillButtons.js.map +1 -1
- package/lib/module/ui/components/payment/PaymentReviewStep.js.map +1 -1
- package/lib/module/ui/components/payment/PaymentSummaryStep.js.map +1 -1
- package/lib/module/ui/context/hooks/useAuthOperations.js +7 -2
- package/lib/module/ui/context/hooks/useAuthOperations.js.map +1 -1
- package/lib/module/ui/hooks/mutations/useServicesMutations.js.map +1 -1
- package/lib/module/ui/hooks/useAuth.js +9 -3
- package/lib/module/ui/hooks/useAuth.js.map +1 -1
- package/lib/module/ui/hooks/useFollow.js +132 -72
- package/lib/module/ui/hooks/useFollow.js.map +1 -1
- package/lib/module/ui/hooks/useWebSSO.js.map +1 -1
- package/lib/module/ui/screens/EditProfileFieldScreen.js +30 -11
- package/lib/module/ui/screens/EditProfileFieldScreen.js.map +1 -1
- package/lib/module/ui/screens/FAQScreen.js +1 -0
- package/lib/module/ui/screens/FAQScreen.js.map +1 -1
- package/lib/module/ui/screens/FeedbackScreen.js.map +1 -1
- package/lib/module/ui/screens/FileManagementScreen.js +0 -1
- package/lib/module/ui/screens/FileManagementScreen.js.map +1 -1
- package/lib/module/ui/screens/HistoryViewScreen.js.map +1 -1
- package/lib/module/ui/screens/PremiumSubscriptionScreen.js.map +1 -1
- package/lib/module/ui/screens/karma/KarmaRewardsScreen.js.map +1 -1
- package/lib/module/ui/stores/fileStore.js +6 -6
- package/lib/module/ui/stores/fileStore.js.map +1 -1
- package/lib/module/ui/utils/fileManagement.js +6 -3
- package/lib/module/ui/utils/fileManagement.js.map +1 -1
- package/lib/typescript/commonjs/ui/components/FollowButton.d.ts +12 -1
- package/lib/typescript/commonjs/ui/components/FollowButton.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/GroupedSection.d.ts +5 -0
- package/lib/typescript/commonjs/ui/components/GroupedSection.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/feedback/types.d.ts +2 -2
- package/lib/typescript/commonjs/ui/components/feedback/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/internal/GroupedPillButtons.d.ts +7 -1
- package/lib/typescript/commonjs/ui/components/internal/GroupedPillButtons.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts +31 -0
- package/lib/typescript/commonjs/ui/hooks/useFollow.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/FAQScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/HistoryViewScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/fileManagement.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/FollowButton.d.ts +12 -1
- package/lib/typescript/module/ui/components/FollowButton.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/GroupedSection.d.ts +5 -0
- package/lib/typescript/module/ui/components/GroupedSection.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/feedback/types.d.ts +2 -2
- package/lib/typescript/module/ui/components/feedback/types.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/internal/GroupedPillButtons.d.ts +7 -1
- package/lib/typescript/module/ui/components/internal/GroupedPillButtons.d.ts.map +1 -1
- package/lib/typescript/module/ui/context/hooks/useAuthOperations.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useAuth.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/useFollow.d.ts +31 -0
- package/lib/typescript/module/ui/hooks/useFollow.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/EditProfileFieldScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/FAQScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/FileManagementScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/HistoryViewScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/fileManagement.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/ui/components/FollowButton.tsx +117 -109
- package/src/ui/components/GroupedSection.tsx +5 -0
- package/src/ui/components/feedback/types.ts +2 -2
- package/src/ui/components/fileManagement/FileDetailsModal.tsx +1 -1
- package/src/ui/components/fileManagement/UploadPreview.tsx +1 -1
- package/src/ui/components/internal/GroupedPillButtons.tsx +17 -8
- package/src/ui/components/payment/PaymentReviewStep.tsx +1 -1
- package/src/ui/components/payment/PaymentSummaryStep.tsx +1 -1
- package/src/ui/context/hooks/useAuthOperations.ts +8 -3
- package/src/ui/hooks/mutations/useServicesMutations.ts +3 -3
- package/src/ui/hooks/useAuth.ts +10 -4
- package/src/ui/hooks/useFollow.ts +161 -74
- package/src/ui/hooks/useWebSSO.ts +3 -3
- package/src/ui/screens/EditProfileFieldScreen.tsx +28 -16
- package/src/ui/screens/FAQScreen.tsx +2 -1
- package/src/ui/screens/FeedbackScreen.tsx +7 -7
- package/src/ui/screens/FileManagementScreen.tsx +1 -2
- package/src/ui/screens/HistoryViewScreen.tsx +5 -1
- package/src/ui/screens/PremiumSubscriptionScreen.tsx +1 -1
- package/src/ui/screens/karma/KarmaRewardsScreen.tsx +1 -1
- package/src/ui/stores/fileStore.ts +9 -9
- package/src/ui/utils/fileManagement.ts +16 -10
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
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 {
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
109
|
+
// Set initial follow status on mount if provided and not already set
|
|
115
110
|
useEffect(() => {
|
|
116
111
|
if (userId && !isFollowing && initiallyFollowing) {
|
|
117
|
-
setFollowStatus
|
|
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
|
|
118
|
+
// Fetch latest follow status from backend on mount
|
|
122
119
|
useEffect(() => {
|
|
123
|
-
if (userId
|
|
124
|
-
fetchStatus
|
|
120
|
+
if (userId) {
|
|
121
|
+
fetchStatus();
|
|
125
122
|
}
|
|
126
|
-
}, [userId, fetchStatus
|
|
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
|
|
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
|
-
|
|
161
|
-
const
|
|
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={[
|
|
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=
|
|
169
|
+
size="small"
|
|
241
170
|
color={isFollowing ? '#FFFFFF' : colors.primary}
|
|
242
171
|
/>
|
|
243
172
|
) : (
|
|
244
|
-
<AnimatedText style={[
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
136
|
+
...(Platform.OS === 'web' ? { whiteSpace: 'nowrap' as const } : null),
|
|
128
137
|
};
|
|
129
138
|
};
|
|
130
139
|
|
|
131
|
-
const getIconColor = (button: ButtonConfig, colors:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
99
|
-
(error
|
|
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
|
|
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
|
|
63
|
-
(s
|
|
62
|
+
const updatedSessions = (previousSessions as ClientSession[]).filter(
|
|
63
|
+
(s) => s.sessionId !== sessionToLogout
|
|
64
64
|
);
|
|
65
65
|
queryClient.setQueryData(queryKeys.sessions.list(), updatedSessions);
|
|
66
66
|
}
|
package/src/ui/hooks/useAuth.ts
CHANGED
|
@@ -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
|
|
121
|
+
const popupSession = await oxyServices.signInWithPopup?.();
|
|
122
122
|
if (popupSession?.user) {
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
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) {
|