Added sentry support
This commit is contained in:
parent
b9802f7b1f
commit
43e9e9790d
8
app.json
8
app.json
@ -68,6 +68,14 @@
|
|||||||
{
|
{
|
||||||
"locationAlwaysAndWhenInUsePermission": "Allow $(PRODUCT_NAME) to use your location."
|
"locationAlwaysAndWhenInUsePermission": "Allow $(PRODUCT_NAME) to use your location."
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@sentry/react-native/expo",
|
||||||
|
{
|
||||||
|
"organization": "gib",
|
||||||
|
"project": "tech-tracker",
|
||||||
|
"url": "https://sentry.gbrown.org/"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
|
@ -5,20 +5,16 @@ import StatusList from '@/components/status/StatusList';
|
|||||||
|
|
||||||
const HomeScreen = () => {
|
const HomeScreen = () => {
|
||||||
return (
|
return (
|
||||||
<ParallaxScrollView
|
<StatusList
|
||||||
headerImage={
|
headerTitle={
|
||||||
<Image source={require('@/assets/images/tech_tracker_logo.png')} style={styles.techTrackerLogo} />
|
<ThemedText style={styles.headerTitle}>
|
||||||
}
|
Tech Tracker
|
||||||
headerTitle={
|
</ThemedText>
|
||||||
<ThemedText type='title' style={styles.headerTitle}>
|
}
|
||||||
Tech Tracker
|
headerImage={
|
||||||
</ThemedText>
|
<Image source={require('@/assets/images/tech_tracker_logo.png')} style={styles.techTrackerLogo} />
|
||||||
}
|
}
|
||||||
>
|
/>
|
||||||
<ThemedView style={styles.titleContainer}>
|
|
||||||
<StatusList />
|
|
||||||
</ThemedView>
|
|
||||||
</ParallaxScrollView>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default HomeScreen;
|
export default HomeScreen;
|
||||||
|
@ -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,
|
|
||||||
}
|
|
||||||
});
|
|
@ -135,6 +135,7 @@ const ProfileScreen = () => {
|
|||||||
placeholder="Enter your full name"
|
placeholder="Enter your full name"
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
fontSize={20}
|
fontSize={20}
|
||||||
|
height={55}
|
||||||
editable={!updating}
|
editable={!updating}
|
||||||
autoCapitalize='words'
|
autoCapitalize='words'
|
||||||
textContentType='name'
|
textContentType='name'
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
||||||
import { useFonts } from 'expo-font';
|
import { useFonts } from 'expo-font';
|
||||||
import { Stack } from 'expo-router';
|
import { Stack, useNavigationContainerRef } from 'expo-router';
|
||||||
import * as SplashScreen from 'expo-splash-screen';
|
import * as SplashScreen from 'expo-splash-screen';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
@ -11,6 +11,22 @@ import { useColorScheme } from '@/hooks/useColorScheme';
|
|||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/lib/supabase';
|
||||||
import Auth from '@/components/auth/Auth';
|
import Auth from '@/components/auth/Auth';
|
||||||
import PushNotificationManager from '@/services/PushNotificationManager';
|
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.
|
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||||
SplashScreen.preventAutoHideAsync();
|
SplashScreen.preventAutoHideAsync();
|
||||||
@ -18,6 +34,7 @@ SplashScreen.preventAutoHideAsync();
|
|||||||
const RootLayout = () => {
|
const RootLayout = () => {
|
||||||
const scheme = useColorScheme() ?? 'dark';
|
const scheme = useColorScheme() ?? 'dark';
|
||||||
const [session, setSession] = useState<Session | null>(null);
|
const [session, setSession] = useState<Session | null>(null);
|
||||||
|
const ref = useNavigationContainerRef();
|
||||||
|
|
||||||
const [loaded] = useFonts({
|
const [loaded] = useFonts({
|
||||||
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
|
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'),
|
||||||
@ -39,6 +56,12 @@ const RootLayout = () => {
|
|||||||
}
|
}
|
||||||
}, [loaded]);
|
}, [loaded]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (ref?.current) {
|
||||||
|
navigationIntegration.registerNavigationContainer(ref);
|
||||||
|
}
|
||||||
|
}, [ref])
|
||||||
|
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -62,4 +85,4 @@ const RootLayout = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RootLayout;
|
export default Sentry.wrap(RootLayout);
|
||||||
|
@ -22,9 +22,20 @@ import debounce from 'lodash/debounce';
|
|||||||
import NetInfo from '@react-native-community/netinfo';
|
import NetInfo from '@react-native-community/netinfo';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
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 scheme = useColorScheme() ?? 'dark';
|
||||||
|
const bottom = useBottomTabOverflow();
|
||||||
|
|
||||||
const [statuses, setStatuses] = useState<UserStatus[]>([]);
|
const [statuses, setStatuses] = useState<UserStatus[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@ -40,6 +51,19 @@ const StatusList = () => {
|
|||||||
const subscriptionRef = useRef<RealtimeChannel | null>(null);
|
const subscriptionRef = useRef<RealtimeChannel | null>(null);
|
||||||
const appStateRef = useRef(AppState.currentState);
|
const appStateRef = useRef(AppState.currentState);
|
||||||
const pendingUpdatesRef = useRef<Set<string>>(new Set());
|
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
|
// Debounced version of the status update handler
|
||||||
const debouncedHandleStatusUpdates = useRef(
|
const debouncedHandleStatusUpdates = useRef(
|
||||||
@ -402,6 +426,25 @@ const StatusList = () => {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}, [formatDate, handleUserSelect, recentlyUpdatedIds]);
|
}, [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
|
// Empty list component
|
||||||
const ListEmptyComponent = useCallback(() => (
|
const ListEmptyComponent = useCallback(() => (
|
||||||
@ -427,13 +470,15 @@ const StatusList = () => {
|
|||||||
<ThemedText style={styles.offlineText}>You are offline</ThemedText>
|
<ThemedText style={styles.offlineText}>You are offline</ThemedText>
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{renderHeader()}
|
||||||
|
|
||||||
<FlatList
|
<AnimatedFlatList
|
||||||
data={statuses}
|
data={statuses}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
enabled={isConnected}
|
enabled={isConnected}
|
||||||
/>
|
/>
|
||||||
@ -447,6 +492,16 @@ const StatusList = () => {
|
|||||||
getItemLayout={(data, index) => (
|
getItemLayout={(data, index) => (
|
||||||
{length: 120, offset: 120 * index, 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 && (
|
{selectedUser && (
|
||||||
@ -467,6 +522,15 @@ const styles = StyleSheet.create({
|
|||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
header: {
|
||||||
|
height: HEADER_HEIGHT,
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
zIndex: 10,
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
loadingContainer: {
|
loadingContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
|
7
metro.config.js
Normal file
7
metro.config.js
Normal 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
1352
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@ -23,14 +23,14 @@
|
|||||||
"@expo/metro-runtime": "~4.0.1",
|
"@expo/metro-runtime": "~4.0.1",
|
||||||
"@expo/ngrok": "4.1.0",
|
"@expo/ngrok": "4.1.0",
|
||||||
"@expo/vector-icons": "^14.0.2",
|
"@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-native-community/netinfo": "11.4.1",
|
||||||
"@react-navigation/bottom-tabs": "^7.2.0",
|
"@react-navigation/bottom-tabs": "^7.2.0",
|
||||||
"@react-navigation/native": "^7.0.14",
|
"@react-navigation/native": "^7.0.14",
|
||||||
"@supabase/supabase-js": "^2.48.1",
|
"@supabase/supabase-js": "^2.48.1",
|
||||||
"aes-js": "^3.1.2",
|
"aes-js": "^3.1.2",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"expo": "~52.0.28",
|
"expo": "~52.0.38",
|
||||||
"expo-apple-authentication": "~7.1.3",
|
"expo-apple-authentication": "~7.1.3",
|
||||||
"expo-auth-session": "~6.0.3",
|
"expo-auth-session": "~6.0.3",
|
||||||
"expo-blur": "~14.0.3",
|
"expo-blur": "~14.0.3",
|
||||||
@ -46,20 +46,20 @@
|
|||||||
"expo-linking": "~7.0.5",
|
"expo-linking": "~7.0.5",
|
||||||
"expo-location": "~18.0.7",
|
"expo-location": "~18.0.7",
|
||||||
"expo-notifications": "~0.29.13",
|
"expo-notifications": "~0.29.13",
|
||||||
"expo-router": "~4.0.17",
|
"expo-router": "~4.0.19",
|
||||||
"expo-secure-store": "~14.0.1",
|
"expo-secure-store": "~14.0.1",
|
||||||
"expo-splash-screen": "~0.29.21",
|
"expo-splash-screen": "~0.29.21",
|
||||||
"expo-status-bar": "~2.0.1",
|
"expo-status-bar": "~2.0.1",
|
||||||
"expo-symbols": "~0.2.1",
|
"expo-symbols": "~0.2.1",
|
||||||
"expo-system-ui": "~4.0.7",
|
"expo-system-ui": "~4.0.7",
|
||||||
"expo-updates": "~0.26.13",
|
"expo-updates": "~0.27.3",
|
||||||
"expo-web-browser": "~14.0.2",
|
"expo-web-browser": "~14.0.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "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-gesture-handler": "~2.20.2",
|
||||||
"react-native-get-random-values": "^1.11.0",
|
"react-native-get-random-values": "^1.11.0",
|
||||||
"react-native-reanimated": "~3.16.1",
|
"react-native-reanimated": "~3.16.1",
|
||||||
@ -68,7 +68,8 @@
|
|||||||
"react-native-svg": "15.8.0",
|
"react-native-svg": "15.8.0",
|
||||||
"react-native-svg-transformer": "^1.5.0",
|
"react-native-svg-transformer": "^1.5.0",
|
||||||
"react-native-web": "~0.19.13",
|
"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": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user