Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Sentry Configuration
# EXPO_PUBLIC_SENTRY_DSN=

# Sentry Source Map Upload (set these as EAS Secrets for production builds)
# SENTRY_AUTH_TOKEN=
# SENTRY_ORG=stepfi-org
# SENTRY_PROJECT=stepfi-app
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Environment secrets
.env
.env.local

node_modules/
.expo/
dist/
Expand Down
10 changes: 9 additions & 1 deletion app.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,15 @@
"expo-router",
"expo-secure-store",
"expo-font",
"expo-local-authentication"
"expo-local-authentication",
[
"@sentry/react-native",
{
"organization": "stepfi-org",
"project": "stepfi-app",
"url": "https://sentry.io/"
}
]
],
"orientation": "portrait",
"icon": "./assets/icon.png",
Expand Down
73 changes: 53 additions & 20 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,19 @@ import { BiometricGate } from '../src/components/BiometricGate';
import { useConnectivityStore } from '../src/offline/connectivity.store';
import { processQueue } from '../src/offline/offline-sync';
import { initPromise } from '../src/locales/i18n';
import { SentryErrorBoundary } from '../components/SentryErrorBoundary';
import {
initSentry,
setSentryUser,
clearSentryUser,
addBreadcrumb,
Sentry,
} from '../services/sentry';
import '../global.css';

// Initialise Sentry as early as possible (module‑level, before any component)
initSentry();

const IDLE_TIMEOUT_MS = 5 * 60 * 1000;

function useAuthGuard() {
Expand All @@ -33,7 +44,22 @@ function useAuthGuard() {
}, [isAuthenticated, isLoading, segments, router]);
}

export default function RootLayout() {
// Sentry user‑context sync — attach / clear wallet identity on auth changes
function useSentryUserContext() {
const walletAddress = useAuthStore((s) => s.walletAddress);
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);

useEffect(() => {
if (isAuthenticated && walletAddress) {
setSentryUser(walletAddress);
addBreadcrumb('auth', 'User context set', { hasWallet: true });
} else {
clearSentryUser();
}
}, [isAuthenticated, walletAddress]);
}

function RootLayout() {
const [i18nReady, setI18nReady] = useState(false);
const hydrate = useAuthStore((s) => s.hydrate);
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
Expand Down Expand Up @@ -66,6 +92,7 @@ export default function RootLayout() {
}, [hydrate]);

useAuthGuard();
useSentryUserContext();

useEffect(() => {
if (!isLoading && isAuthenticated && !biometricCheckDone) {
Expand All @@ -84,6 +111,8 @@ export default function RootLayout() {

useEffect(() => {
const subscription = AppState.addEventListener('change', (state) => {
addBreadcrumb('app.lifecycle', `AppState → ${state}`);

if (state === 'active') {
const elapsed = Date.now() - lastActiveRef.current;
if (
Expand Down Expand Up @@ -140,24 +169,28 @@ export default function RootLayout() {
}

return (
<SafeAreaProvider>
{isLocked && isAuthenticated ? (
<BiometricGate />
) : (
<View
style={{ flex: 1 }}
onTouchStart={() => {
if (!useSecurityStore.getState().isLocked) {
startIdleTimer();
}
}}
>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(auth)" />
<Stack.Screen name="(tabs)" />
</Stack>
</View>
)}
</SafeAreaProvider>
<SentryErrorBoundary>
<SafeAreaProvider>
{isLocked && isAuthenticated ? (
<BiometricGate />
) : (
<View
style={{ flex: 1 }}
onTouchStart={() => {
if (!useSecurityStore.getState().isLocked) {
startIdleTimer();
}
}}
>
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(auth)" />
<Stack.Screen name="(tabs)" />
</Stack>
</View>
)}
</SafeAreaProvider>
</SentryErrorBoundary>
);
}

export default Sentry.wrap(RootLayout);
76 changes: 76 additions & 0 deletions components/SentryErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React from 'react';
import { View, Text, Pressable, StyleSheet } from 'react-native';
import * as Sentry from '@sentry/react-native';

interface FallbackProps {
resetError: () => void;
}

function ErrorFallback({ resetError }: FallbackProps) {
return (
<View style={styles.container}>
<Text style={styles.emoji}>⚠️</Text>
<Text style={styles.title}>Something went wrong</Text>
<Text style={styles.subtitle}>
An unexpected error occurred. Our team has been notified.
</Text>
<Pressable style={styles.button} onPress={resetError}>
<Text style={styles.buttonText}>Reload App</Text>
</Pressable>
</View>
);
}

interface Props {
children: React.ReactNode;
}

export function SentryErrorBoundary({ children }: Props) {
return (
<Sentry.ErrorBoundary
fallback={({ resetError }) => <ErrorFallback resetError={resetError} />}
showDialog={false}
>
{children}
</Sentry.ErrorBoundary>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#080F1A',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 32,
},
emoji: {
fontSize: 48,
marginBottom: 16,
},
title: {
fontSize: 22,
fontWeight: '700',
color: '#FFFFFF',
marginBottom: 8,
textAlign: 'center',
},
subtitle: {
fontSize: 15,
color: '#94A3B8',
textAlign: 'center',
lineHeight: 22,
marginBottom: 32,
},
button: {
backgroundColor: '#6366F1',
paddingHorizontal: 32,
paddingVertical: 14,
borderRadius: 12,
},
buttonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: '600',
},
});
Loading
Loading