Added sentry support

This commit is contained in:
Gabriel Brown 2025-03-17 09:35:27 -05:00
parent b9802f7b1f
commit 43e9e9790d
9 changed files with 732 additions and 1013 deletions

View File

@ -68,6 +68,14 @@
{
"locationAlwaysAndWhenInUsePermission": "Allow $(PRODUCT_NAME) to use your location."
}
],
[
"@sentry/react-native/expo",
{
"organization": "gib",
"project": "tech-tracker",
"url": "https://sentry.gbrown.org/"
}
]
],
"experiments": {

View File

@ -5,20 +5,16 @@ import StatusList from '@/components/status/StatusList';
const HomeScreen = () => {
return (
<ParallaxScrollView
headerImage={
<Image source={require('@/assets/images/tech_tracker_logo.png')} style={styles.techTrackerLogo} />
}
headerTitle={
<ThemedText type='title' style={styles.headerTitle}>
Tech Tracker
</ThemedText>
}
>
<ThemedView style={styles.titleContainer}>
<StatusList />
</ThemedView>
</ParallaxScrollView>
<StatusList
headerTitle={
<ThemedText style={styles.headerTitle}>
Tech Tracker
</ThemedText>
}
headerImage={
<Image source={require('@/assets/images/tech_tracker_logo.png')} style={styles.techTrackerLogo} />
}
/>
);
};
export default HomeScreen;

View File

@ -1,241 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
StyleSheet,
Alert,
ActivityIndicator,
SafeAreaView,
ScrollView,
} from 'react-native';
import { supabase } from '@/lib/supabase';
import { ThemedView, ThemedText, ThemedTextButton, ThemedTextInput } from '@/components/theme';
import ProfileAvatar from '@/components/auth/Profile_Avatar';
import LogoutButton from '@/components/auth/Logout_Button';
import { useFocusEffect } from '@react-navigation/native';
import ParallaxScrollView from '@/components/default/ParallaxScrollView';
import { IconSymbol } from '@/components/ui/IconSymbol';
const SettingsScreen = () => {
const [loading, setLoading] = useState(true);
const [updating, setUpdating] = useState(false);
const [profile, setProfile] = useState({
full_name: '',
email: '',
avatar_url: null,
provider: ''
});
// Fetch profile when screen comes into focus
useFocusEffect(
React.useCallback(() => {
fetchUserProfile();
}, [])
);
const fetchUserProfile = async () => {
setLoading(true);
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
throw new Error('Not authenticated');
}
const { data, error } = await supabase
.from('profiles')
.select('*')
.eq('id', user.id)
.single();
if (error) throw error;
if (data) {
setProfile({
full_name: data.full_name || '',
email: data.email || '',
avatar_url: data.avatar_url,
provider: data.provider || ''
});
}
} catch (error) {
console.error('Error fetching profile:', error);
Alert.alert('Error', 'Failed to load profile information');
} finally {
setLoading(false);
}
};
const updateProfile = async () => {
setUpdating(true);
try {
const { data: { user } } = await supabase.auth.getUser();
if (!user) throw new Error('Not authenticated');
// Validate input
if (!profile.full_name.trim()) {
Alert.alert('Error', 'Please enter your full name');
return;
}
const updates = {
id: user.id,
full_name: profile.full_name.trim(),
updated_at: new Date(),
};
const { error } = await supabase
.from('profiles')
.upsert(updates);
if (error) throw error;
Alert.alert('Success', 'Profile updated successfully!');
} catch (error) {
Alert.alert('Error', error instanceof Error ? error.message : 'Failed to update profile');
} finally {
setUpdating(false);
}
};
const handleAvatarUpload = () => {
// Refresh profile data after avatar upload
fetchUserProfile();
};
if (loading) {
return (
<ThemedView style={styles.loadingContainer}>
<ActivityIndicator size="large" />
</ThemedView>
);
}
return (
<ParallaxScrollView
headerImage={
<IconSymbol size={80} color='#808080' name='gear.circle' style={styles.headerImage} />
}
headerTitle={
<ThemedText type='title' style={styles.headerTitle}>
Settings
</ThemedText>
}
>
<ThemedView style={styles.container}>
<ProfileAvatar
url={profile.avatar_url}
size={120}
onUpload={handleAvatarUpload}
disabled={updating}
/>
{profile.provider && (
<ThemedText style={styles.providerText}>
Signed in with {profile.provider.charAt(0).toUpperCase() + profile.provider.slice(1)}
</ThemedText>
)}
<SafeAreaView style={styles.formSection}>
<ThemedView style={styles.formSection}>
<ThemedText type='title' style={styles.label}>Name</ThemedText>
<ThemedTextInput
value={profile.full_name}
onChangeText={(text) => setProfile(prev => ({ ...prev, full_name: text }))}
placeholder="Enter your full name"
style={styles.input}
editable={!updating}
autoCapitalize='words'
textContentType='name'
maxLength={50}
onSubmitEditing={updateProfile}
returnKeyType='send'
/>
</ThemedView>
</SafeAreaView>
<ThemedTextButton
text={updating ? 'Saving...' : 'Save Changes'}
onPress={updateProfile}
disabled={updating || !profile.full_name.trim()}
fontSize={18}
fontWeight='semibold'
width='90%'
style={styles.saveButton}
/>
<LogoutButton
fontSize={18}
fontWeight='semibold'
width='90%'
style={styles.logoutButton}
/>
</ThemedView>
</ParallaxScrollView>
);
};
export default SettingsScreen;
const styles = StyleSheet.create({
headerImage: {
color: '#808080',
bottom: 6,
left: 38,
position: 'absolute',
},
headerTitle: {
position: 'absolute',
bottom: 20,
left: 16,
right: 0,
textAlign: 'center',
fontSize: 48,
lineHeight: 64,
fontWeight: 'bold',
},
scrollContainer: {
flexGrow: 1,
},
container: {
flex: 1,
padding: 16,
alignItems: 'center',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
formSection: {
marginBottom: 20,
},
label: {
marginBottom: 8,
fontSize: 16,
fontWeight: '500',
},
input: {
fontSize: 16,
paddingVertical: 12,
paddingHorizontal: 10,
borderRadius: 8,
marginBottom: 20,
width: '100%',
},
disabledInput: {
opacity: 0.7,
},
saveButton: {
borderRadius: 8,
alignItems: 'center',
marginBottom: 10,
},
logoutButton: {
marginTop: 30,
borderRadius: 8,
alignItems: 'center',
},
providerText: {
marginBottom: 20,
fontSize: 14,
opacity: 0.7,
}
});

View File

@ -135,6 +135,7 @@ const ProfileScreen = () => {
placeholder="Enter your full name"
style={styles.input}
fontSize={20}
height={55}
editable={!updating}
autoCapitalize='words'
textContentType='name'

View File

@ -1,6 +1,6 @@
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { useFonts } from 'expo-font';
import { Stack } from 'expo-router';
import { Stack, useNavigationContainerRef } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { StatusBar } from 'expo-status-bar';
import { useEffect, useState } from 'react';
@ -11,6 +11,22 @@ import { useColorScheme } from '@/hooks/useColorScheme';
import { supabase } from '@/lib/supabase';
import Auth from '@/components/auth/Auth';
import PushNotificationManager from '@/services/PushNotificationManager';
import * as Sentry from '@sentry/react-native';
import { isRunningInExpoGo } from 'expo';
const navigationIntegration = Sentry.reactNavigationIntegration({
enableTimeToInitialDisplay: !isRunningInExpoGo(),
});
Sentry.init({
dsn: process.env.EXPO_PUBLIC_SENTRY_DSN,
debug: true,
tracesSampleRate: 1.0,
integrations: [
navigationIntegration,
],
enableNativeFramesTracking: !isRunningInExpoGo(),
});
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();
@ -18,6 +34,7 @@ SplashScreen.preventAutoHideAsync();
const RootLayout = () => {
const scheme = useColorScheme() ?? 'dark';
const [session, setSession] = useState<Session | null>(null);
const ref = useNavigationContainerRef();
const [loaded] = useFonts({
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
@ -39,6 +56,12 @@ const RootLayout = () => {
}
}, [loaded]);
useEffect(() => {
if (ref?.current) {
navigationIntegration.registerNavigationContainer(ref);
}
}, [ref])
if (!loaded) {
return null;
}
@ -62,4 +85,4 @@ const RootLayout = () => {
);
};
export default RootLayout;
export default Sentry.wrap(RootLayout);

View File

@ -22,9 +22,20 @@ import debounce from 'lodash/debounce';
import NetInfo from '@react-native-community/netinfo';
import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useBottomTabOverflow } from '@/components/ui/TabBarBackground';
const StatusList = () => {
const HEADER_HEIGHT = 150;
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
type StatusListProps = {
headerImage?: React.ReactNode;
headerTitle?: React.ReactNode;
};
const StatusList = ({headerImage, headerTitle}: StatusListProps) => {
const scheme = useColorScheme() ?? 'dark';
const bottom = useBottomTabOverflow();
const [statuses, setStatuses] = useState<UserStatus[]>([]);
const [loading, setLoading] = useState(true);
@ -40,6 +51,19 @@ const StatusList = () => {
const subscriptionRef = useRef<RealtimeChannel | null>(null);
const appStateRef = useRef(AppState.currentState);
const pendingUpdatesRef = useRef<Set<string>>(new Set());
// Parallax animation setup
const scrollY = useRef(new Animated.Value(0)).current;
const headerTranslateY = scrollY.interpolate({
inputRange: [0, HEADER_HEIGHT],
outputRange: [0, -HEADER_HEIGHT/2],
extrapolate: 'clamp',
});
const headerScale = scrollY.interpolate({
inputRange: [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
outputRange: [2, 1, 1],
extrapolate: 'clamp',
});
// Debounced version of the status update handler
const debouncedHandleStatusUpdates = useRef(
@ -402,6 +426,25 @@ const StatusList = () => {
</TouchableOpacity>
);
}, [formatDate, handleUserSelect, recentlyUpdatedIds]);
// Render the header component
const renderHeader = () => (
<Animated.View
style={[
styles.header,
{
backgroundColor: Colors[scheme].accent,
transform: [
{ translateY: headerTranslateY },
{ scale: headerScale }
]
}
]}
>
{headerImage}
{headerTitle}
</Animated.View>
);
// Empty list component
const ListEmptyComponent = useCallback(() => (
@ -427,13 +470,15 @@ const StatusList = () => {
<ThemedText style={styles.offlineText}>You are offline</ThemedText>
</ThemedView>
)}
{renderHeader()}
<FlatList
<AnimatedFlatList
data={statuses}
keyExtractor={(item) => item.id}
refreshControl={
<RefreshControl
refreshing={refreshing}
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
enabled={isConnected}
/>
@ -447,6 +492,16 @@ const StatusList = () => {
getItemLayout={(data, index) => (
{length: 120, offset: 120 * index, index}
)}
contentContainerStyle={{
paddingTop: HEADER_HEIGHT, // Add padding to account for the header
paddingBottom: bottom,
paddingHorizontal: 16,
}}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: scrollY } } }],
{ useNativeDriver: true }
)}
scrollEventThrottle={16}
/>
{selectedUser && (
@ -467,6 +522,15 @@ const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
height: HEADER_HEIGHT,
position: 'absolute',
top: 0,
left: 0,
right: 0,
zIndex: 10,
overflow: 'hidden',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',

7
metro.config.js Normal file
View File

@ -0,0 +1,7 @@
// This replaces `const { getDefaultConfig } = require('expo/metro-config');`
const { getSentryExpoConfig } = require('@sentry/react-native/metro');
// This replaces `const config = getDefaultConfig(__dirname);`
const config = getSentryExpoConfig(__dirname);
module.exports = config;

1352
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -23,14 +23,14 @@
"@expo/metro-runtime": "~4.0.1",
"@expo/ngrok": "4.1.0",
"@expo/vector-icons": "^14.0.2",
"@react-native-async-storage/async-storage": "^1.23.1",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-community/netinfo": "11.4.1",
"@react-navigation/bottom-tabs": "^7.2.0",
"@react-navigation/native": "^7.0.14",
"@supabase/supabase-js": "^2.48.1",
"aes-js": "^3.1.2",
"date-fns": "^4.1.0",
"expo": "~52.0.28",
"expo": "~52.0.38",
"expo-apple-authentication": "~7.1.3",
"expo-auth-session": "~6.0.3",
"expo-blur": "~14.0.3",
@ -46,20 +46,20 @@
"expo-linking": "~7.0.5",
"expo-location": "~18.0.7",
"expo-notifications": "~0.29.13",
"expo-router": "~4.0.17",
"expo-router": "~4.0.19",
"expo-secure-store": "~14.0.1",
"expo-splash-screen": "~0.29.21",
"expo-status-bar": "~2.0.1",
"expo-symbols": "~0.2.1",
"expo-system-ui": "~4.0.7",
"expo-updates": "~0.26.13",
"expo-updates": "~0.27.3",
"expo-web-browser": "~14.0.2",
"jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0",
"lodash": "^4.17.21",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.6",
"react-native": "0.76.7",
"react-native-gesture-handler": "~2.20.2",
"react-native-get-random-values": "^1.11.0",
"react-native-reanimated": "~3.16.1",
@ -68,7 +68,8 @@
"react-native-svg": "15.8.0",
"react-native-svg-transformer": "^1.5.0",
"react-native-web": "~0.19.13",
"react-native-webview": "13.12.5"
"react-native-webview": "13.12.5",
"@sentry/react-native": "~6.3.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",