Function bodies 80 total
EmailSignup function · typescript · L12-L150 (139 LOC)app/(auth)/email-signup.tsx
export default function EmailSignup() {
const router = useRouter();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [focusedInput, setFocusedInput] = useState<string | null>(null);
const handleSignup = async () => {
console.log("DEBUG: Sign Up Button Pressed");
if (!email || !password) {
Alert.alert('Required', 'Please enter both email and password.');
return;
}
setLoading(true);
try {
console.log("DEBUG: Attempting Supabase Sign Up");
const { data, error } = await supabase.auth.signUp({
email: email,
password: password,
});
console.log("DEBUG: Supabase Response", { data, error });
if (error) throw error;
if (data.session) {
console.log("Signup success, session active");
} eAuthLayout function · typescript · L3-L13 (11 LOC)app/(auth)/_layout.tsx
export default function AuthLayout() {
return (
<Stack
screenOptions={{
headerShown: false,
contentStyle: { backgroundColor: '#FFFFFF' }, // Light theme
animation: 'fade',
}}
/>
);
}SignupScreen function · typescript · L43-L356 (314 LOC)app/(auth)/signup.tsx
export default function SignupScreen() {
const router = useRouter();
const [loading, setLoading] = React.useState(false);
const logoAnim = useRef(new Animated.Value(0)).current;
const cardAnim = useRef(new Animated.Value(0)).current;
const cardSlide = useRef(new Animated.Value(24)).current;
const emailAnim = useRef(new Animated.Value(0)).current;
const dividerAnim = useRef(new Animated.Value(0)).current;
const googleAnim = useRef(new Animated.Value(0)).current;
const appleAnim = useRef(new Animated.Value(0)).current;
const bottomAnim = useRef(new Animated.Value(0)).current;
const anim = (val: Animated.Value, toValue: number, duration: number, delay: number) =>
Animated.timing(val, { toValue, duration, delay, useNativeDriver: true });
useEffect(() => {
Animated.parallel([
anim(logoAnim, 1, 400, 0),
anim(cardAnim, 1, 500, 200),
anim(cardSlide, 0, 500, 200),
HistoryDetailScreen function · typescript · L144-L406 (263 LOC)app/bill/history/[id].tsx
export default function HistoryDetailScreen() {
const router = useRouter();
const { id, billData: billDataParam } = useLocalSearchParams<{ id: string; billData: string }>();
const [isItemsExpanded, setIsItemsExpanded] = useState(false);
const bill: BillData | null = useMemo(() => {
if (billDataParam) {
try { return JSON.parse(billDataParam); }
catch (e) { console.error('Failed to parse billData', e); }
}
return null;
}, [billDataParam]);
if (!bill) {
return (
<SafeAreaView style={{ flex: 1, backgroundColor: '#f9f9ff', alignItems: 'center', justifyContent: 'center' }} edges={['top']}>
<Stack.Screen options={{ headerShown: false }} />
<Text style={{ color: '#484554', fontWeight: '500' }}>Bill not found.</Text>
<TouchableOpacity
onPress={() => router.back()}
style={{ marginTop: 16, paddingHorizontal: 24, paddirenderGuestView function · typescript · L609-L809 (201 LOC)app/bill/payment.tsx
function renderGuestView() {
return (
<View>
{/* Badge + Title */}
<View style={{ alignItems: 'center', marginBottom: 20 }}>
<View style={{
backgroundColor: COLORS.secondaryContainer,
paddingHorizontal: 14, paddingVertical: 6,
borderRadius: 999, marginBottom: 12,
}}>
<Text style={{
fontSize: 10, fontWeight: '800', color: COLORS.onSecondaryContainer,
letterSpacing: 2, textTransform: 'uppercase',
}}>
Review Payment
</Text>
</View>
<Text style={{ fontSize: 24, fontWeight: '800', color: COLORS.onSurface, textAlign: 'center' }}>
Settle with {hostParticipant?.name || 'Host'}
</Text>
renderHostView function · typescript · L813-L898 (86 LOC)app/bill/payment.tsx
function renderHostView() {
return (
<View>
{/* Title */}
<View style={{ marginBottom: 24 }}>
<Text style={{
fontSize: 10, fontWeight: '800', color: COLORS.primary,
letterSpacing: 2.5, textTransform: 'uppercase', marginBottom: 10,
}}>
Payment Status
</Text>
<Text style={{ fontSize: 32, fontWeight: '800', color: COLORS.onSurface, letterSpacing: -0.5 }}>
{allPaymentsSettled ? 'All Settled! \uD83C\uDF89' : 'Waiting for Payments'}
</Text>
</View>
{/* Participant List */}
{participants.map((participant, index) => {
const isParticipantHost = participant.user_id === hostId;
const request = paymentRequests.find(
pr => pr.from_user_id =renderStatusBadge function · typescript · L900-L973 (74 LOC)app/bill/payment.tsx
function renderStatusBadge(status: string, request?: PaymentRequest) {
switch (status) {
case 'host':
return (
<View style={{
backgroundColor: COLORS.greenBg, paddingHorizontal: 14, paddingVertical: 7,
borderRadius: 999,
}}>
<Text style={{ fontSize: 10, fontWeight: '800', color: COLORS.green, textTransform: 'uppercase', letterSpacing: 1 }}>
You
</Text>
</View>
);
case 'confirmed':
return (
<View style={{
backgroundColor: COLORS.greenBg, paddingHorizontal: 14, paddingVertical: 7,
borderRadius: 999, flexDirection: 'row', alignItems: 'center', gap: 4,
}}>
<Check size={12} color={COLORS.green} />
Same scanner, your repo: https://repobility.com — Repobility
ParticipantSetupScreen function · typescript · L27-L192 (166 LOC)app/bill/setup.tsx
export default function ParticipantSetupScreen() {
const router = useRouter();
const { billData } = useLocalSearchParams<{ billData: string }>();
const [name, setName] = useState('');
// Default "You" user - Purple for light theme
const [participants, setParticipants] = useState<User[]>([
{
id: 'u1',
name: 'You',
avatar: 'https://i.pravatar.cc/150?u=u1',
color: '#B54CFF',
initials: 'ME'
}
]);
const handleAddUser = () => {
if (!name.trim()) return;
const newId = `u${participants.length + 1}-${Date.now()}`;
// Cycle through colors
const color = COLORS[(participants.length - 1) % COLORS.length];
const initials = name.trim().slice(0, 2).toUpperCase();
const newUser: User = {
id: newId,
name: name.trim(),
avatar: `https://i.pravatar.cc/150?u=${newId}`,
color: color,
initials: inSuccessScreen function · typescript · L11-L308 (298 LOC)app/bill/success.tsx
export default function SuccessScreen() {
const router = useRouter();
const { billId, totalAmount, groupSize } = useLocalSearchParams<{
billId: string;
totalAmount: string;
groupSize: string;
}>();
const [groupPhoto, setGroupPhoto] = useState<string | null>(null);
const [isUploading, setIsUploading] = useState(false);
const [uploadComplete, setUploadComplete] = useState(false);
// TODO: Calculate real points from Supabase rewards system
const points = 125;
const handleTakePhoto = async () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
const { status } = await ImagePicker.requestCameraPermissionsAsync();
if (status !== 'granted') {
Alert.alert('Permission Needed', 'Camera access is required to take a group photo.');
return;
}
const result = await ImagePicker.launchCameraAsync({
mediaTypes: ['images'],
allowsEditing: true,
CaptureScreen function · typescript · L13-L371 (359 LOC)app/camera/capture.tsx
export default function CaptureScreen() {
const router = useRouter();
const { user, session, profile } = useAuth();
const [permission, requestPermission] = useCameraPermissions();
const [capturedImage, setCapturedImage] = useState<string | null>(null);
const [isAnalyzing, setIsAnalyzing] = useState(false);
const cameraRef = useRef<CameraView>(null);
// Request permission on mount
useEffect(() => {
if (!permission?.granted) {
requestPermission();
}
}, [permission]);
const handleCapture = async () => {
if (!cameraRef.current) return;
try {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
const photo = await cameraRef.current.takePictureAsync({
quality: 0.8,
base64: true,
});
if (photo?.uri) {
setCapturedImage(photo.uri);
}
} catch (error) {
console.error('Error capturCameraLayout function · typescript · L3-L13 (11 LOC)app/camera/_layout.tsx
export default function CameraLayout() {
return (
<Stack
screenOptions={{
headerShown: false,
contentStyle: { backgroundColor: '#000000' },
animation: 'fade',
}}
/>
);
}Root function · typescript · L7-L28 (22 LOC)app/+html.tsx
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
{/*
Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
*/}
<ScrollViewStyleReset />
{/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
<style dangerouslySetInnerHTML={{ __html: responsiveBackground }} />
{/* Add any additional <head> elements that you want globally available on web... */}
</head>
<body>{children}</body>
</html>
);
}RootRedirect function · typescript · L12-L36 (25 LOC)app/index.tsx
export default function RootRedirect() {
const { isLoading, session, hasProfile } = useAuth();
// Wait for auth to complete before deciding where to go
if (isLoading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#FFFFFF' }}>
<ActivityIndicator size="large" color="#B54CFF" />
</View>
);
}
// No session → login
if (!session) {
return <Redirect href="/(auth)/login" />;
}
// No profile → setup
if (!hasProfile) {
return <Redirect href="/(auth)/setup" />;
}
// Has session + profile → tabs
return <Redirect href="/(tabs)" />;
}LoadingScreen function · typescript · L13-L20 (8 LOC)app/_layout.tsx
function LoadingScreen() {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center", backgroundColor: "#FFFFFF" }}>
<StatusBar style="dark" />
<ActivityIndicator size="large" color="#B54CFF" />
</View>
);
}DeepLinkHandler function · typescript · L23-L209 (187 LOC)app/_layout.tsx
function DeepLinkHandler() {
const { session, user, profile, isLoading } = useAuth();
const router = useRouter();
const processingRef = useRef(false);
const handleDeepLink = useCallback(async (url: string) => {
// Prevent multiple simultaneous processing
if (processingRef.current) return;
console.log('DeepLink: Processing URL:', url);
// Handle OAuth callback - extract tokens from URL
// Supabase returns tokens in URL fragment (hash) or as code parameter
if (url.includes('auth/callback') || url.includes('#access_token=') || url.includes('?code=')) {
console.log('DeepLink: OAuth callback detected');
console.log('DeepLink: Full URL:', url);
processingRef.current = true;
try {
// Try to extract tokens from URL fragment (hash)
const hashIndex = url.indexOf('#');
if (hashIndex !== -1) {
const hashParams = url.substring(hashIndex + 1);
const params = new URLSearchParams(hashParams);
Repobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
NavigationController function · typescript · L212-L260 (49 LOC)app/_layout.tsx
function NavigationController() {
const { session, isLoading, hasOnboarded } = useAuth();
const segments = useSegments() as string[];
const router = useRouter();
useEffect(() => {
// Don't navigate while loading - this is CRITICAL
if (isLoading) {
console.log('Navigation: Still loading, waiting...');
return;
}
const inAuthGroup = segments[0] === "(auth)";
const inTabsGroup = segments[0] === "(tabs)";
const inBillGroup = segments[0] === "bill";
const inOnboardingGroup = segments[0] === "onboarding";
console.log('Navigation: session=', !!session, 'hasOnboarded=', hasOnboarded, 'segments=', segments);
if (!session) {
// No session - must go to login
if (!inAuthGroup) {
console.log('Navigation: No session, redirecting to login');
router.replace("/(auth)/login");
}
} else if (!hasOnboarded) {
// Has session but hasn't completed onboarding.
// Allowed screens: /(auth)/setup (Step 1) ProtectedLayout function · typescript · L263-L280 (18 LOC)app/_layout.tsx
function ProtectedLayout() {
const { isLoading } = useAuth();
// CRITICAL: Block rendering until auth check is complete
// This prevents flash of wrong screens
if (isLoading) {
return <LoadingScreen />;
}
return (
<>
<StatusBar style="dark" />
<NavigationController />
<DeepLinkHandler />
<Slot />
</>
);
}RootLayout function · typescript · L282-L303 (22 LOC)app/_layout.tsx
export default function RootLayout() {
const [fontsLoaded] = useFonts({
Outfit_400Regular,
Outfit_500Medium,
Outfit_700Bold,
});
if (!fontsLoaded) {
return null;
}
return (
<StripeProvider
publishableKey={process.env.EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY || ''}
merchantIdentifier="merchant.com.theraq17.divvit"
>
<AuthProvider>
<ProtectedLayout />
</AuthProvider>
</StripeProvider>
);
}ModalScreen function · typescript · L7-L18 (12 LOC)app/modal.tsx
export default function ModalScreen() {
return (
<View style={styles.container}>
<Text style={styles.title}>Modal</Text>
<View style={styles.separator} lightColor="#eee" darkColor="rgba(255,255,255,0.1)" />
<EditScreenInfo path="app/modal.tsx" />
{/* Use a light status bar on iOS to account for the black space above the modal */}
<StatusBar style={Platform.OS === 'ios' ? 'light' : 'auto'} />
</View>
);
}NotFoundScreen function · typescript · L6-L19 (14 LOC)app/+not-found.tsx
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: 'Oops!' }} />
<View style={styles.container}>
<Text style={styles.title}>This screen doesn't exist.</Text>
<Link href="/" style={styles.link}>
<Text style={styles.linkText}>Go to home screen!</Text>
</Link>
</View>
</>
);
}GroupLobbiesScreen function · typescript · L15-L284 (270 LOC)app/onboarding/group-lobbies.tsx
export default function GroupLobbiesScreen() {
const router = useRouter();
const handleNext = async () => {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
router.push('/onboarding/social-finance');
};
const handleSkip = async () => {
await Haptics.selectionAsync();
router.replace('/(tabs)');
};
return (
<SafeAreaView style={{ flex: 1, backgroundColor: '#f9f9ff' }} edges={['top', 'bottom']}>
<StatusBar style="dark" />
{/* Ambient glow orbs */}
<View style={{
position: 'absolute', top: -60, right: -60,
width: SCREEN_WIDTH * 0.55, height: SCREEN_WIDTH * 0.55,
borderRadius: 9999, backgroundColor: 'rgba(75,41,180,0.05)',
}} />
<View style={{
position: 'absolute', bottom: -80, left: -60,
width: SCREEN_WIDTH * 0.65, height: SCREEN_WIDTH * 0.65,
borderRadiusSplashScreen function · typescript · L18-L195 (178 LOC)app/onboarding/index.tsx
export default function SplashScreen() {
const router = useRouter();
const logoOpacity = useSharedValue(0);
const logoTranslate = useSharedValue(30);
const wordmarkOpacity = useSharedValue(0);
const wordmarkTranslate = useSharedValue(30);
const dividerOpacity = useSharedValue(0);
const taglineOpacity = useSharedValue(0);
const buttonsOpacity = useSharedValue(0);
const buttonsTranslate = useSharedValue(40);
const bottomTagOpacity = useSharedValue(0);
useEffect(() => {
const cfg = { duration: 600, easing: Easing.out(Easing.exp) };
logoOpacity.value = withDelay(100, withTiming(1, cfg));
logoTranslate.value = withDelay(100, withTiming(0, cfg));
wordmarkOpacity.value = withDelay(250, withTiming(1, cfg));
wordmarkTranslate.value = withDelay(250, withTiming(0, cfg));
dividerOpacity.value = withDelay(400, withTiming(1, { duration: 500 }));
taglineOpacity.value = withDelay(500, withTiming(1, { OnboardingLayout function · typescript · L3-L13 (11 LOC)app/onboarding/_layout.tsx
export default function OnboardingLayout() {
return (
<Stack
screenOptions={{
headerShown: false,
contentStyle: { backgroundColor: '#FFFFFF' },
animation: 'fade',
}}
/>
);
}Repobility · MCP-ready · https://repobility.com
PersonalInfoScreen function · typescript · L40-L394 (355 LOC)app/onboarding/personal-info.tsx
export default function PersonalInfoScreen() {
const router = useRouter();
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [phone, setPhone] = useState('');
const [country, setCountry] = useState('');
const [dob, setDob] = useState<Date | null>(null);
const [showCountryPicker, setShowCountryPicker] = useState(false);
const [showDatePicker, setShowDatePicker] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
const validateForm = useCallback(() => {
const newErrors: Record<string, string> = {};
if (!firstName.trim()) {
newErrors.firstName = 'First name is required';
}
if (!lastName.trim()) {
newErrors.lastName = 'Last name is required';
}
if (!phone.trim()) {
newErrors.phone = 'Phone number is required';
} else if (!/^\+?[\d\s-]{10,}$/.test(phone.replace(/\s/g, ''))) {
useDebounce function · typescript · L21-L35 (15 LOC)app/onboarding/setup.tsx
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}SocialFinanceScreen function · typescript · L21-L254 (234 LOC)app/onboarding/social-finance.tsx
export default function SocialFinanceScreen() {
const router = useRouter();
const headerOpacity = useSharedValue(0);
const cardOpacity = useSharedValue(0);
const cardTranslate = useSharedValue(16);
const glassOpacity = useSharedValue(0);
const glassTranslate = useSharedValue(12);
const headlineOpacity = useSharedValue(0);
const subtitleOpacity = useSharedValue(0);
const buttonOpacity = useSharedValue(0);
const buttonTranslate = useSharedValue(12);
useEffect(() => {
const ease = (dur: number) => ({ duration: dur, easing: Easing.out(Easing.exp) });
headerOpacity.value = withDelay(200, withTiming(1, ease(600)));
cardOpacity.value = withDelay(600, withTiming(1, ease(700)));
cardTranslate.value = withDelay(600, withTiming(0, ease(700)));
glassOpacity.value = withDelay(1000, withTiming(1, ease(600)));
glassTranslate.value = withDelay(1000, withTiming(0, ease(600)));
HistoryScreen function · typescript · L452-L595 (144 LOC)app/(tabs)/history.tsx
export default function HistoryScreen() {
const { user, session, isLoading: isAuthLoading } = useAuth();
const [bills, setBills] = useState<Bill[]>([]);
const [billPaymentRequests, setBillPaymentRequests] = useState<Record<string, PaymentRequestRow[]>>({});
const [isLoading, setIsLoading] = useState(true);
const [activeIndex, setActiveIndex] = useState(0);
const flatListRef = useRef<FlatList>(null);
const handleShare = useCallback(async (bill: Bill) => {
try {
const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
const d = new Date(bill.created_at);
const dateStr = `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
const { details, total_amount } = bill;
const displayTotal = (total_amount && total_amount > 0)
? total_amount
: ((details?.subtotal || 0) + (details?.tax || 0) + (details?.tip || 0));
consHomeScreen function · typescript · L78-L345 (268 LOC)app/(tabs)/index.tsx
export default function HomeScreen() {
const router = useRouter();
const { isLoading: isAuthLoading, session, user, profile } = useAuth();
const { points, totalSplit, minutesSaved, recentActivity, drafts, isLoading, deleteDraft, refetch } = useHomeStats();
// Refetch drafts when screen is focused (e.g., returning from draft save)
useFocusEffect(
useCallback(() => {
refetch();
}, [refetch])
);
// CRITICAL: If auth is still loading, show a full loading screen
// This prevents flash of empty/mock data on web reload
if (isAuthLoading) {
return (
<SafeAreaView className="flex-1 bg-background justify-center items-center">
<ActivityIndicator size="large" color="#4b29b4" />
<Text className="text-on-surface-variant font-body text-sm mt-4">Loading...</Text>
</SafeAreaView>
);
}
// If auth finished but no session, show nothing (NavigationController will redirect)
if (!session) {
return (
<SafeAreaView classNameTabLayout function · typescript · L5-L69 (65 LOC)app/(tabs)/_layout.tsx
export default function TabLayout() {
return (
<Tabs
initialRouteName="index"
screenOptions={{
headerShown: false,
tabBarStyle: {
backgroundColor: '#FFFFFF',
borderTopColor: '#E5E7EB',
borderTopWidth: 1,
height: 85,
paddingBottom: 25,
paddingTop: 10,
},
tabBarActiveTintColor: '#B54CFF',
tabBarInactiveTintColor: '#9CA3AF',
tabBarLabelStyle: {
fontFamily: 'Outfit_500Medium',
fontSize: 11,
marginTop: 4,
},
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color, focused }) => (
<Home
size={24}
color={color}
strokeWidth={focused ? 2.5 : 2}
/>
),
}}
/>
<Tabs.Screen
name="history"
options={{
title: 'History',
tabBarIcon: ({ coloProfileScreen function · typescript · L26-L249 (224 LOC)app/(tabs)/profile.tsx
export default function ProfileScreen() {
const router = useRouter();
const { session, user, profile, signOut, refreshProfile } = useAuth();
const [isEditing, setIsEditing] = useState(false);
const [phone, setPhone] = useState('');
const [venmoHandle, setVenmoHandle] = useState('');
const [cashappHandle, setCashappHandle] = useState('');
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
// Initialize form values from profile
useEffect(() => {
if (profile) {
setPhone(profile.phone || '');
setVenmoHandle(profile.venmo_handle || '');
setCashappHandle(profile.cashapp_handle || '');
}
}, [profile]);
const onRefresh = useCallback(async () => {
setRefreshing(true);
await refreshProfile();
setRefreshing(false);
}, [refreshProfile]);
const handleEdit = async () => {
await Haptics.selectionAsync();
ReceiptItem class · python · L15-L19 (5 LOC)backend/app/api/endpoints/receipts.py
class ReceiptItem(BaseModel):
"""Individual item from a receipt."""
name: str
price: float
quantity: int = 1Hi, dataset curator — please cite Repobility (https://repobility.com) when reusing this data.
ScanResponse class · python · L22-L28 (7 LOC)backend/app/api/endpoints/receipts.py
class ScanResponse(BaseModel):
"""Response model for receipt scan results."""
items: List[ReceiptItem]
subtotal: Optional[float] = None
tax: Optional[float] = None
total: Optional[float] = None
scanned_tip: Optional[float] = Nonescan_receipt function · python · L32-L73 (42 LOC)backend/app/api/endpoints/receipts.py
async def scan_receipt(file: UploadFile = File(...)):
"""
Scan a receipt image and extract structured data.
Args:
file: Uploaded receipt image file (JPEG, PNG, etc.)
Returns:
Parsed receipt data with items and totals
"""
print(f"[Backend] Received scan request — file: {file.filename!r}, type: {file.content_type!r}")
# Validate file type
allowed_types = ["image/jpeg", "image/png", "image/webp", "image/heic"]
if file.content_type not in allowed_types:
print(f"[Backend] Rejected — invalid content type: {file.content_type!r}")
raise HTTPException(
status_code=400,
detail=f"Invalid file type. Allowed types: {', '.join(allowed_types)}"
)
try:
# Read file contents
contents = await file.read()
print(f"[Backend] File read — {len(contents)} bytes. Calling Gemini...")
# Process with Gemini
gemini_service = GeminiService()
result = await geminSettings class · python · L10-L27 (18 LOC)backend/app/core/config.py
class Settings(BaseSettings):
"""Application settings loaded from environment variables."""
# API Keys
gemini_api_key: str = ""
# Application settings
app_name: str = "Divvit Backend"
debug: bool = False
# Cloud Run settings
port: int = 8080
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
case_sensitive = False
extra = "ignore" # Ignore Expo/frontend env varsConfig class · python · L23-L27 (5 LOC)backend/app/core/config.py
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
case_sensitive = False
extra = "ignore" # Ignore Expo/frontend env varsget_settings function · python · L31-L36 (6 LOC)backend/app/core/config.py
def get_settings() -> Settings:
"""
Get cached settings instance.
Uses lru_cache to ensure settings are only loaded once.
"""
return Settings()GeminiService class · python · L19-L111 (93 LOC)backend/app/services/gemini.py
class GeminiService:
"""Service class for interacting with Google's Gemini AI."""
def __init__(self):
"""Initialize the Gemini service with API credentials."""
api_key = settings.gemini_api_key or os.environ.get("GEMINI_API_KEY", "")
self.client = genai.Client(api_key=api_key)
self.model_name = "gemini-2.5-flash"
async def parse_receipt(
self, image_data: bytes, content_type: str = "image/jpeg"
) -> Dict[str, Any]:
"""
Parse a receipt image using Gemini Vision.
Args:
image_data: Raw image bytes
content_type: MIME type of the image
Returns:
Dictionary containing parsed receipt data
"""
# Validate image with PIL
try:
image = Image.open(io.BytesIO(image_data))
image.verify() # Verify it's a valid image
except Exception as e:
raise ValueError(f"Invalid image file: {e}")
# Construct the p__init__ method · python · L22-L26 (5 LOC)backend/app/services/gemini.py
def __init__(self):
"""Initialize the Gemini service with API credentials."""
api_key = settings.gemini_api_key or os.environ.get("GEMINI_API_KEY", "")
self.client = genai.Client(api_key=api_key)
self.model_name = "gemini-2.5-flash"parse_receipt method · python · L28-L111 (84 LOC)backend/app/services/gemini.py
async def parse_receipt(
self, image_data: bytes, content_type: str = "image/jpeg"
) -> Dict[str, Any]:
"""
Parse a receipt image using Gemini Vision.
Args:
image_data: Raw image bytes
content_type: MIME type of the image
Returns:
Dictionary containing parsed receipt data
"""
# Validate image with PIL
try:
image = Image.open(io.BytesIO(image_data))
image.verify() # Verify it's a valid image
except Exception as e:
raise ValueError(f"Invalid image file: {e}")
# Construct the prompt for receipt parsing
prompt = """Extract items (name, price), tax, tip, and total from this receipt.
Return ONLY valid JSON in this exact format:
{
"items": [
{"name": "item name", "price": 0.00, "quantity": 1}
],
"subtotal": 0.00,
"tax": 0.00,
"total": 0.00,
"scanned_tip": 0.00
}
Rules:
- Return ONLY the rawSame scanner, your repo: https://repobility.com — Repobility
root function · python · L39-L41 (3 LOC)backend/main.py
async def root():
"""Health check endpoint."""
return {"status": "healthy", "service": "divvit-backend"}health_check function · python · L45-L50 (6 LOC)backend/main.py
async def health_check():
"""Detailed health check for Cloud Run."""
return {
"status": "ok",
"gemini_configured": bool(settings.gemini_api_key),
}BillHeader function · typescript · L42-L92 (51 LOC)components/bill/BillHeader.tsx
export default function BillHeader({ subtotal, taxAmount, taxInput, setTaxInput, setTaxAmount, billTotal, progressSegments }: Props) {
return (
<View className="mb-8 mt-2">
<View className="flex-row justify-between items-end mb-6">
<View className="flex-1 flex-shrink mr-4">
<Text className="font-semibold text-on-surface-variant uppercase tracking-widest text-[11px] mb-1">Subtotal</Text>
<Text className="text-4xl font-extrabold tracking-tight text-on-surface">${subtotal.toFixed(2)}</Text>
</View>
<View className="items-end">
<View className="flex-row items-center mb-1">
<Text className="text-on-surface-variant text-sm mr-1">Tax</Text>
<Text className="font-medium text-sm text-on-surface">$</Text>
<TextInput
value={taxInput || (taxAmount > 0 ? taxAmount.toFixeBillItemCard function · typescript · L47-L131 (85 LOC)components/bill/BillItemCard.tsx
export default function BillItemCard({
item, index, priceInput, uniqueAssignees, activeUsers,
onNameChange, onPriceChange, onPriceBlur, onAssignToggle, onDelete, setSwipeableRef
}: Props) {
return (
<Animated.View
entering={FadeInDown.delay(index * 30).springify()}
layout={Layout.springify()}
className="mb-3"
>
<Swipeable
ref={setSwipeableRef}
renderRightActions={() => <RenderRightActions onDelete={onDelete} />}
overshootRight={false}
friction={2}
>
<TouchableOpacity
onPress={onAssignToggle}
activeOpacity={0.9}
className="bg-surface-container-lowest p-5 rounded-xl flex-row items-center justify-between w-full"
>
<View className="flex-row items-center flex-1">
<View className="w-12 h-12 rounded-xl bgParticipantSelector function · typescript · L20-L78 (59 LOC)components/bill/ParticipantSelector.tsx
export default function ParticipantSelector({ activeUsers, selectedUserIds, onSelectUser, onAddUser }: Props) {
const selectedCount = selectedUserIds.length;
const label = selectedCount === 0
? 'Tap a person to start'
: selectedCount === 1
? 'Assigning to'
: `Assigning to ${selectedCount} people (split evenly)`;
return (
<View className="mt-8 pb-4">
<Text className="font-semibold text-on-surface-variant uppercase tracking-widest text-[11px] mb-4">{label}</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ paddingRight: 20 }}>
<View className="flex-row items-center gap-3">
{activeUsers.map(user => {
const isSelected = selectedUserIds.includes(user.id);
return (
<TouchableOpacity
key={user.id}
QuickActionsGrid function · typescript · L11-L36 (26 LOC)components/bill/QuickActionsGrid.tsx
export default function QuickActionsGrid({ onSplitEvenly, onRandomize, onClear }: Props) {
return (
<View className="mt-6 bg-surface-container-low rounded-2xl p-4 flex-row gap-3">
<TouchableOpacity onPress={onSplitEvenly} activeOpacity={0.8} className="flex-1 py-4 items-center">
<View className="w-10 h-10 rounded-full bg-white items-center justify-center mb-2" style={{ shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.08, shadowRadius: 2, elevation: 1 }}>
<Columns color="#4b29b4" size={20} />
</View>
<Text className="text-[10px] font-bold uppercase tracking-tight text-on-surface-variant text-center">Split Evenly</Text>
</TouchableOpacity>
<TouchableOpacity onPress={onRandomize} activeOpacity={0.8} className="flex-1 py-4 items-center">
<View className="w-10 h-10 rounded-full bg-white items-center justify-center mb-2" style={{ shaContextCard function · typescript · L19-L62 (44 LOC)components/bill/tip/ContextCard.tsx
export default function ContextCard({ restaurantName, contextDescription, users }: Props) {
// Only show up to 2 avatars to match design
const displayUsers = users.slice(0, 2);
const extraUsersCount = users.length > 2 ? users.length - 2 : 0;
return (
<View className="bg-primary p-6 rounded-[32px] overflow-hidden justify-between mb-4">
<View className="z-10">
<View className="mb-2">
<Utensils size={32} color="#ffffff" strokeWidth={1.5} />
</View>
<Text className="font-heading font-bold text-xl text-on-primary ml-1">{restaurantName || 'Restaurant'}</Text>
<Text className="opacity-80 text-sm text-on-primary ml-1 mt-1">{contextDescription || 'Bill Split'}</Text>
</View>
<View className="mt-8 flex-row items-center z-10 ml-1">
<View className="flex-row">
{displayUsers.map((user, index) => (
FinalCalculationSurface function · typescript · L13-L59 (47 LOC)components/bill/tip/FinalCalculationSurface.tsx
export default function FinalCalculationSurface({ tipLabel, tipAmount, total, onContinue, isLoading = false }: Props) {
return (
<View className="bg-[#dce2f7]/30 rounded-[40px] p-8 mt-2 mb-4">
<View className="flex-row justify-between items-center mb-6">
<Text className="text-on-surface-variant font-medium">Selected Tip ({tipLabel})</Text>
<Text className="font-bold text-on-surface">${tipAmount.toFixed(2)}</Text>
</View>
<View className="flex-row justify-between items-center mb-6">
<Text className="text-on-surface-variant font-medium">Total</Text>
<Text className="font-heading text-2xl font-bold text-on-surface">${total.toFixed(2)}</Text>
</View>
<View className="pt-6 border-t border-[#cac4d6]/50" style={{ paddingHorizontal: 16, width: '100%' }}>
<TouchableOpacity
onPress={onContinue}
disabled={iRepobility's GitHub App fixes findings like these · https://github.com/apps/repobility-bot
TipSelection function · typescript · L18-L147 (130 LOC)components/bill/tip/TipSelection.tsx
export default function TipSelection({
tipBaseAmount,
selectedPercentage,
customTip,
noTip,
onSelectPercentage,
onCustomChange,
isScannedTipActive,
tipPercentages
}: Props) {
const inputRef = useRef<TextInput>(null);
const isCustomActive = !selectedPercentage && !!customTip && !noTip;
return (
<View className="mb-3">
<Text className="text-sm font-heading font-extrabold uppercase tracking-widest text-on-surface-variant mb-6 px-2">
Choose a Tip
</Text>
{/* Cards grid — paddingTop reserves space for the MOST COMMON badge */}
<View className="flex-row flex-wrap justify-between" style={{ opacity: noTip ? 0.4 : 1, paddingTop: 14 }}>
{tipPercentages.map((tip) => {
const isSelected = selectedPercentage === tip.value && !noTip;
const isMostCommon = tip.value === 0.18;
const isScannedMatch = isScannedTipATotalsCard function · typescript · L10-L29 (20 LOC)components/bill/tip/TotalsCard.tsx
export default function TotalsCard({ subtotal, tax, dueNow }: Props) {
return (
<View className="bg-surface-container-low p-6 rounded-[32px] mb-4">
<View className="flex-row justify-between items-center mb-4">
<Text className="text-on-surface-variant font-medium text-sm">Subtotal</Text>
<Text className="font-bold text-on-surface text-base">${subtotal.toFixed(2)}</Text>
</View>
<View className="flex-row justify-between items-center mb-4">
<Text className="text-on-surface-variant font-medium text-sm">Tax</Text>
<Text className="font-bold text-on-surface text-base">${tax.toFixed(2)}</Text>
</View>
<View className="pt-4 border-t border-[#cac4d6]/30 flex-row justify-between items-end">
<Text className="text-on-surface-variant text-sm pb-1">Due Now</Text>
<Text className="text-4xl font-heading font-extrabold tracking-tighter tDivvitHeader function · typescript · L4-L10 (7 LOC)components/DivvitHeader.tsx
export default function DivvitHeader() {
return (
<View style={styles.header}>
<Text style={styles.wordmark}>Divvit</Text>
</View>
);
}page 1 / 2next ›