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
26 changes: 17 additions & 9 deletions app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Root Layout
*
* Main app layout that provides navigation stack, theme providers,
* and global context for ThumbCode.
* global context, and error handling for ThumbCode.
*/

import { Stack, useRouter, useSegments } from 'expo-router';
Expand All @@ -11,9 +11,15 @@ import { useEffect } from 'react';
import { ActivityIndicator, View } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { ErrorBoundary } from '@/components/error';
import { OnboardingProvider, useOnboarding } from '@/contexts/onboarding';
import { logger, setupGlobalErrorHandlers } from '@/lib';
import '../global.css';

// Initialize global error handlers
setupGlobalErrorHandlers();
logger.info('ThumbCode app started');

function RootLayoutNav() {
const { isLoading, hasCompletedOnboarding } = useOnboarding();
const segments = useSegments();
Expand Down Expand Up @@ -85,13 +91,15 @@ function RootLayoutNav() {

export default function RootLayout() {
return (
<OnboardingProvider>
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<StatusBar style="light" />
<RootLayoutNav />
</SafeAreaProvider>
</GestureHandlerRootView>
</OnboardingProvider>
<ErrorBoundary>
<OnboardingProvider>
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<StatusBar style="light" />
<RootLayoutNav />
</SafeAreaProvider>
</GestureHandlerRootView>
</OnboardingProvider>
</ErrorBoundary>
);
}
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,15 @@
"generate:all": "pnpm --filter @thumbcode/dev-tools run generate:all"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.32.0",
"@react-native-async-storage/async-storage": "^2.0.0",
"@react-native-community/netinfo": "^11.4.1",
"@react-navigation/native": "^7.0.0",
Comment on lines +35 to +38
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

@react-native-community/netinfo 11.4.1 React Native 0.76.0 compatibility requirements

💡 Result:

Short answer: Yes — @react-native-community/netinfo 11.4.1 is compatible with React Native 0.76. The package supports React Native >= 0.60 (autolinking) and lists react-native as a peer dependency; RN 0.76 meets that requirement. [1][2]

Sources:

  • package README / npm (React Native compatibility: supports RN 0.60+ with autolinking). [1]
  • package metadata for v11.4.1 (peerDependency react-native; v11.4.1 published). [2]

References:
[1] npm package README for @react-native-community/netinfo.
[2] jsDocs / npm metadata for @react-native-community/netinfo v11.4.1.


🌐 Web query:

@react-native-async-storage/async-storage 2.0.0 React Native 0.76.0 compatibility

💡 Result:

Short answer: Generally yes — @react-native-async-storage/async-storage v2.x supports React Native >=0.60 (so it should work with RN 0.76.0). [1][2]

Notes and cautions:

  • The official docs state it requires RN 0.60+ (autolinking etc.). [1]
  • There are reported Android build issues for version 2.0.0 related to Kotlin/KSP/Kotlin-2 toolchain (you may hit "ksp is too old for kotlin-2" errors). If you use Async Storage 2.0.0 on RN 0.76.0 and your Android toolchain uses Kotlin 2.x, you may need to either upgrade the library to a later patch that fixes Kotlin compatibility or align/downgrade your Kotlin/KSP versions per the issue discussion. [2]
  • The npm package currently has newer 2.x releases (e.g., 2.2.0); prefer the latest 2.x patch release to avoid known 2.0.0 bugs. [3]

Recommendations:

  • Use the latest @react-native-async-storage/async-storage 2.x (not exactly 2.0.0) if possible. [3]
  • If you must stay on 2.0.0 and see Kotlin/KSP errors, align your Android Kotlin/KSP plugin to the versions compatible with the library (or downgrade Kotlin to 1.9.x) as discussed in the GitHub issue. [2]

Sources:
[1] Async Storage install docs (requires RN 0.60+).
[2] GitHub issue reporting Kotlin/KSP compatibility with version 2.0.0.
[3] npm package page showing newer 2.x releases (e.g., 2.2.0).


Upgrade @react-native-async-storage/async-storage to the latest 2.x patch version (≥2.2.0).

While @react-native-community/netinfo@^11.4.1 is compatible with React Native 0.76.0, @react-native-async-storage/async-storage@2.0.0 has documented Android build issues with Kotlin 2.x/KSP toolchains (reported as "ksp is too old for kotlin-2" errors). Upgrade to a newer 2.x patch release (e.g., 2.2.0 or later) to avoid potential Android build failures.

🤖 Prompt for AI Agents
In `@package.json` around lines 35 - 38, Update the dependency version for
`@react-native-async-storage/async-storage` in package.json from "^2.0.0" to a
newer 2.x patch (e.g., "^2.2.0" or later) to avoid Kotlin/KSP Android build
issues; edit the dependency entry for
"@react-native-async-storage/async-storage" in package.json and run your package
manager's install command to lock the updated version.

"@thumbcode/config": "workspace:*",
"@thumbcode/core": "workspace:*",
"@thumbcode/state": "workspace:*",
"@thumbcode/types": "workspace:*",
"@thumbcode/ui": "workspace:*",
"@anthropic-ai/sdk": "^0.32.0",
"@react-native-async-storage/async-storage": "^2.0.0",
"@react-navigation/native": "^7.0.0",
"babel-preset-expo": "^54.0.9",
"date-fns": "^4.1.0",
"diff": "^8.0.3",
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

108 changes: 108 additions & 0 deletions src/components/error/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/**
* Error Boundary Component
*
* React error boundary that catches errors in child components
* and displays a fallback UI.
*/

import { Component, type ErrorInfo, type ReactNode } from 'react';
import { logger } from '@/lib/logger';
import { ErrorFallback } from './ErrorFallback';

interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
resetKeys?: unknown[];
}

interface State {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}

export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}

static getDerivedStateFromError(error: Error): Partial<State> {
return { hasError: true, error };
}

componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
// Log the error
logger.error('React Error Boundary caught an error', error, {
componentStack: errorInfo.componentStack,
});

// Update state with error info
this.setState({ errorInfo });

// Call optional error callback
this.props.onError?.(error, errorInfo);
}

componentDidUpdate(prevProps: Props): void {
// Reset error state when resetKeys change
if (this.state.hasError && this.props.resetKeys) {
const hasResetKeyChanged = this.props.resetKeys.some(
(key, index) => key !== prevProps.resetKeys?.[index]
);

if (hasResetKeyChanged) {
this.resetErrorBoundary();
}
}
}

resetErrorBoundary = (): void => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
});
};

render(): ReactNode {
if (this.state.hasError) {
// Use custom fallback if provided
if (this.props.fallback) {
return this.props.fallback;
}

// Use default error fallback
return (
<ErrorFallback
error={this.state.error}
componentStack={this.state.errorInfo?.componentStack}
onRetry={this.resetErrorBoundary}
/>
);
}

return this.props.children;
}
}

/**
* Hook-friendly wrapper for error boundary
*/
export function withErrorBoundary<P extends object>(
WrappedComponent: React.ComponentType<P>,
fallback?: ReactNode
) {
return function WithErrorBoundary(props: P) {
return (
<ErrorBoundary fallback={fallback}>
<WrappedComponent {...props} />
</ErrorBoundary>
);
};
}
161 changes: 161 additions & 0 deletions src/components/error/ErrorFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/**
* Error Fallback Component
*
* User-friendly error display with retry functionality.
* Follows ThumbCode's organic design language.
*/

import { Alert, Pressable, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Container, VStack } from '@/components/layout';
import { Text } from '@/components/ui';

interface ErrorFallbackProps {
error: Error | null;
componentStack?: string | null;
onRetry?: () => void;
onReportIssue?: () => void;
title?: string;
message?: string;
}

export function ErrorFallback({
error,
componentStack,
onRetry,
onReportIssue,
title = 'Something went wrong',
message = "We're sorry, but something unexpected happened. Please try again.",
}: ErrorFallbackProps) {
const insets = useSafeAreaInsets();
const isDev = __DEV__;

const handleReportIssue = () => {
if (onReportIssue) {
onReportIssue();
} else {
// TODO: Implement proper issue reporting (e.g., open GitHub issues URL)
Alert.alert('Report Issue', 'Issue reporting will be available in a future update.');
}
};

return (
<View
className="flex-1 bg-charcoal"
style={{ paddingTop: insets.top, paddingBottom: insets.bottom }}
>
<Container padding="lg" className="flex-1 justify-center">
<VStack spacing="lg" align="center">
{/* Error Icon */}
<View
className="w-20 h-20 bg-coral-500/20 items-center justify-center"
style={{
borderTopLeftRadius: 40,
borderTopRightRadius: 36,
borderBottomRightRadius: 42,
borderBottomLeftRadius: 38,
}}
>
<Text className="text-4xl">⚠️</Text>
</View>

{/* Error Title */}
<Text size="xl" weight="bold" className="text-white text-center font-display">
{title}
</Text>

{/* Error Message */}
<Text className="text-neutral-400 text-center max-w-xs">{message}</Text>

{/* Dev-only Error Details */}
{isDev && error && (
<View
className="bg-surface p-4 w-full max-w-sm"
style={{
borderTopLeftRadius: 12,
borderTopRightRadius: 10,
borderBottomRightRadius: 14,
borderBottomLeftRadius: 8,
}}
>
<Text size="sm" weight="semibold" className="text-coral-500 mb-2">
Debug Info
</Text>
<Text size="sm" className="text-neutral-400 font-mono mb-2">
{error.name}: {error.message}
</Text>
{componentStack && (
<Text size="xs" className="text-neutral-500 font-mono" numberOfLines={8}>
{componentStack}
</Text>
)}
</View>
)}

{/* Retry Button */}
{onRetry && (
<Pressable
onPress={onRetry}
className="bg-coral-500 px-8 py-3 active:bg-coral-600"
style={{
borderTopLeftRadius: 24,
borderTopRightRadius: 22,
borderBottomRightRadius: 26,
borderBottomLeftRadius: 20,
}}
>
<Text weight="semibold" className="text-white">
Try Again
</Text>
</Pressable>
)}

{/* Secondary Action */}
<Pressable className="py-2" onPress={handleReportIssue}>
<Text size="sm" className="text-teal-500">
Report Issue
</Text>
</Pressable>
</VStack>
</Container>
</View>
);
}

/**
* Compact error fallback for inline use
*/
interface CompactErrorFallbackProps {
message?: string;
onRetry?: () => void;
}

export function CompactErrorFallback({
message = 'Failed to load',
onRetry,
}: CompactErrorFallbackProps) {
return (
<View
className="bg-surface/50 p-4"
style={{
borderTopLeftRadius: 12,
borderTopRightRadius: 10,
borderBottomRightRadius: 14,
borderBottomLeftRadius: 8,
}}
>
<VStack spacing="sm" align="center">
<Text size="sm" className="text-neutral-400">
{message}
</Text>
{onRetry && (
<Pressable onPress={onRetry}>
<Text size="sm" className="text-teal-500">
Tap to retry
</Text>
</Pressable>
)}
</VStack>
</View>
);
}
8 changes: 8 additions & 0 deletions src/components/error/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Error Components
*
* Components for error handling and display.
*/

export { ErrorBoundary, withErrorBoundary } from './ErrorBoundary';
export { CompactErrorFallback, ErrorFallback } from './ErrorFallback';
Loading