diff --git a/GoogleService-Info.plist b/GoogleService-Info.plist
index 7fb687e..fce29b4 100644
--- a/GoogleService-Info.plist
+++ b/GoogleService-Info.plist
@@ -25,9 +25,9 @@
IS_SIGNIN_ENABLED
CLIENT_ID
- 906784991953-aoq1tgiac6ge9aqv4kguai26265393h9.apps.googleusercontent.com
+ 982053776531-0gqv0n84kjid9vtu00iq9drjvf9dvjhn.apps.googleusercontent.com
REVERSED_CLIENT_ID
- com.googleusercontent.apps.906784991953-aoq1tgiac6ge9aqv4kguai26265393h9
+ com.googleusercontent.apps.982053776531-0gqv0n84kjid9vtu00iq9drjvf9dvjhn
GOOGLE_APP_ID
1:982053776531:ios:eec4ee1692d95d2ce3b166
diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx
index 677eb4e..de96b2d 100644
--- a/app/(tabs)/_layout.tsx
+++ b/app/(tabs)/_layout.tsx
@@ -16,26 +16,17 @@ export default function TabLayout() {
return (
,
- headerRight: () => (
-
-
- {({ pressed }) => (
-
- )}
-
-
- ),
}}
/>
- My Header
-
- This is some example text to show a plain page component.
-
-
-
- );
+ return (
+
+
+
+ My Header
+ This is some example text to show a plain page component.
+
+
+
+ );
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- justifyContent: "center",
- alignItems: "center",
- padding: 16,
- backgroundColor: "#121212",
- },
- header: {
- fontSize: 24,
- fontWeight: "bold",
- color: "#fff",
- marginBottom: 20,
- },
- text: {
- fontSize: 16,
- color: "#ccc",
- textAlign: "center",
- },
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 16,
+ backgroundColor: '#121212',
+ },
+ header: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: '#fff',
+ marginBottom: 20,
+ },
+ text: {
+ fontSize: 16,
+ color: '#ccc',
+ textAlign: 'center',
+ },
});
diff --git a/app/(tabs)/cellular.tsx.new b/app/(tabs)/cellular.tsx.new
new file mode 100644
index 0000000..e69de29
diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx
index 7e9b748..ffe7219 100644
--- a/app/(tabs)/index.tsx
+++ b/app/(tabs)/index.tsx
@@ -6,6 +6,7 @@ import LogViewer from '@/components/LogViewer';
import BatteryInfo from '@/components/BatteryInfo';
import { triggerLocalSampleNotification } from '@/utils/notifications.utils';
import { usePushNotifications } from '@/hooks/usePushNotifications';
+import CustomHeader from '@/components/CustomHeader';
export default function LocationComponent() {
const { fcmToken } = usePushNotifications();
@@ -14,77 +15,80 @@ export default function LocationComponent() {
const textColor = '#ffffff';
return (
-
-
-
+
+
+
+
+
-
+
- {/* Battery information */}
-
+ {/* Battery information */}
+
- {/* Location / Map */}
- {loading ? (
-
- ) : error ? (
- {error}
- ) : location && address ? (
- <>
-
- Address: {address.name}, {address.city}, {address.region}, {address.country}
-
-
- {location.coords.latitude} - {location.coords.longitude}
-
+ {/* Location / Map */}
+ {loading ? (
+
+ ) : error ? (
+ {error}
+ ) : location && address ? (
+ <>
+
+ Address: {address.name}, {address.city}, {address.region}, {address.country}
+
+
+ {location.coords.latitude} - {location.coords.longitude}
+
-
-
- {location && (
-
- )}
-
-
- >
- ) : (
- Waiting for location...
- )}
+
+
+ {location && (
+
+ )}
+
+
+ >
+ ) : (
+ Waiting for location...
+ )}
-
-
+
+
- {/* Link to Login page */}
-
-
-
-
+ {/* Link to Login page */}
+
+
+
+
+
-
-
+
+
);
}
diff --git a/app/(tabs)/jobs.tsx b/app/(tabs)/jobs.tsx
index 1250953..57a1166 100644
--- a/app/(tabs)/jobs.tsx
+++ b/app/(tabs)/jobs.tsx
@@ -1,237 +1,316 @@
-import React, { useState, useEffect } from "react";
-import {
- View,
- Text,
- TouchableOpacity,
- StyleSheet,
- ActivityIndicator,
- ScrollView,
- Linking,
- RefreshControl,
-} from "react-native";
-
-export default function PaginatedPostsCards() {
- const [posts, setPosts] = useState([]);
- const [page, setPage] = useState(1);
- const [pageSize] = useState(10); // Customize page size here
- const [totalCount, setTotalCount] = useState(0);
- const [loading, setLoading] = useState(false);
- const [refreshing, setRefreshing] = useState(false); // Add refreshing state
- const [error, setError] = useState(null);
-
- const totalPages = Math.ceil(totalCount / pageSize);
-
- const fetchPosts = async () => {
- try {
- setLoading(true);
- setError(null);
-
- const response = await fetch(
- `https://new.codebuilder.org/api/reddit/posts?page=${page}&pageSize=${pageSize}`
- );
- if (!response.ok) {
- throw new Error(`Error: ${response.statusText}`);
- }
- const data = await response.json();
-
- setPosts(data.data || []);
- setTotalCount(data.totalCount || 0);
- } catch (err) {
- setError(
- err instanceof Error ? err.message : "An unknown error occurred"
- );
- } finally {
- setLoading(false);
- }
- };
-
- const handleRefresh = async () => {
- try {
- setRefreshing(true);
- setPage(1); // Reset to the first page when refreshing
- await fetchPosts();
- } finally {
- setRefreshing(false);
- }
- };
-
- useEffect(() => {
- fetchPosts();
- }, [page]);
-
- const handleNextPage = () => {
- if (page < totalPages) {
- setPage((prev) => prev + 1);
- }
- };
-
- const handlePrevPage = () => {
- if (page > 1) {
- setPage((prev) => prev - 1);
- }
- };
-
- const handleOpenURL = (url: string) => {
- if (url) {
- Linking.openURL(url).catch((err) =>
- console.error("Failed to open URL:", err)
- );
- }
- };
-
- return (
-
- Reddit Posts (Page {page})
-
- {loading && (
-
- )}
-
- {error && Error: {error}}
-
-
+import React, { useState, useEffect } from 'react';
+import { View, Text, TouchableOpacity, StyleSheet, ActivityIndicator, ScrollView, Linking, RefreshControl } from 'react-native';
+import CustomHeader from '@/components/CustomHeader';
+
+// Define types for Job and related data
+interface Company {
+ id: number;
+ name: string;
+}
+
+interface JobTag {
+ id: number;
+ tag: {
+ id: number;
+ name: string;
+ };
+}
+
+interface JobMetadata {
+ id: number;
+ name: string;
+ value: string;
+}
+
+interface JobSource {
+ id: number;
+ source: string;
+ externalId?: string;
+ rawUrl?: string;
+}
+
+interface Job {
+ id: number;
+ title: string;
+ company?: Company;
+ author?: string;
+ location?: string;
+ url: string;
+ postedAt?: string;
+ description?: string;
+ isRemote?: boolean;
+ createdAt: string;
+ updatedAt: string;
+ tags: JobTag[];
+ metadata: JobMetadata[];
+ sources: JobSource[];
+}
+
+export default function JobsListView() {
+ const [jobs, setJobs] = useState([]);
+ const [page, setPage] = useState(1);
+ const [pageSize] = useState(10);
+ const [totalCount, setTotalCount] = useState(0);
+ const [loading, setLoading] = useState(false);
+ const [refreshing, setRefreshing] = useState(false);
+ const [error, setError] = useState(null);
+
+ const totalPages = Math.ceil(totalCount / pageSize);
+
+ const fetchJobs = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ // Calculate skip value based on page
+ const skipValue = (page - 1) * pageSize;
+
+ // Log API request for debugging
+ console.log(`Fetching jobs with: skip=${skipValue}, first=${pageSize}, page=${page}`);
+
+ // Use the new API endpoint with pagination parameters
+ const response = await fetch(`https://api.codebuilder.org/jobs?skip=${skipValue}&first=${pageSize}`);
+
+ if (!response.ok) {
+ throw new Error(`Error: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+
+ // Log response data for debugging
+ console.log(`Received ${data.jobs?.length || 0} jobs, total: ${data.totalCount || 0}`);
+
+ setJobs(data.jobs || []);
+ setTotalCount(data.totalCount || 0);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'An unknown error occurred');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleRefresh = async () => {
+ try {
+ setRefreshing(true);
+ setPage(1); // Reset to the first page when refreshing
+ await fetchJobs();
+ } finally {
+ setRefreshing(false);
+ }
+ };
+
+ // Initialize component
+ useEffect(() => {
+ console.log('Component mounted, fetching initial data');
+ fetchJobs();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []); // Empty dependency array means this only runs once on mount
+
+ // Handle page changes
+ useEffect(() => {
+ if (page > 1) {
+ // Skip on initial load (page 1 is already loaded)
+ console.log(`Page changed to ${page}, fetching new data`);
+ fetchJobs();
}
- >
- {!loading &&
- !error &&
- posts.length > 0 &&
- posts.map((post: any) => (
- handleOpenURL(post.url)}
- >
- {post.title}
- by {post.author}
-
- Subreddit:
- {post.subreddit}
-
-
- Upvotes:
- {post.upvotes}
- Downvotes:
- {post.downvotes}
-
-
- Posted At:
-
- {new Date(post.postedAt).toLocaleString()}
-
-
-
- ))}
-
-
-
-
- Previous
-
-
-
- Page {page} of {totalPages || 1}
-
-
-
- Next
-
-
-
- );
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [page]); // Only run when page changes
+
+ const handleNextPage = () => {
+ if (page < totalPages) {
+ console.log(`Moving to next page: ${page + 1}`);
+ setPage(page + 1); // Use direct value to ensure immediate update
+ }
+ };
+
+ const handlePrevPage = () => {
+ if (page > 1) {
+ console.log(`Moving to previous page: ${page - 1}`);
+ setPage(page - 1); // Use direct value to ensure immediate update
+ }
+ };
+
+ const handleOpenURL = (url: string) => {
+ if (url) {
+ Linking.openURL(url).catch((err) => console.error('Failed to open URL:', err));
+ }
+ };
+
+ // Helper function to get job metadata value by name
+ const getMetadataValue = (job: Job, name: string): string | null => {
+ const metadata = job.metadata.find((m) => m.name === name);
+ return metadata ? metadata.value : null;
+ };
+
+ // Helper function to get job source
+ const getJobSource = (job: Job): string => {
+ if (job.sources && job.sources.length > 0) {
+ return job.sources[0].source;
+ }
+ return 'Unknown';
+ };
+
+ return (
+
+
+
+ Jobs (Page {page})
+
+ {loading && }
+
+ {error && Error: {error}}
+
+ }>
+ {!loading &&
+ !error &&
+ jobs.length > 0 &&
+ jobs.map((job) => (
+ handleOpenURL(job.url)}>
+ {job.title}
+
+
+ Company:
+ {job.company?.name || 'Unknown'}
+
+
+ {job.author && (
+
+ Posted by:
+ {job.author}
+
+ )}
+
+ {job.location && (
+
+ Location:
+ {job.location}
+
+ )}
+
+
+ Remote:
+ {job.isRemote ? 'Yes' : 'No'}
+
+
+ {job.tags.length > 0 && (
+
+ Skills:
+ {job.tags.map((tag) => tag.tag.name).join(', ')}
+
+ )}
+
+
+ Source:
+ {getJobSource(job)}
+
+
+
+ Posted At:
+ {job.postedAt ? new Date(job.postedAt).toLocaleString() : 'Unknown'}
+
+
+ ))}
+
+ {!loading && !error && jobs.length === 0 && No jobs found}
+
+
+
+
+ Previous
+
+
+
+ Page {page} of {totalPages || 1}
+
+
+
+ Next
+
+
+
+
+ );
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- paddingTop: 5,
- paddingHorizontal: 16,
- backgroundColor: "#121212",
- },
- scrollContainer: {
- marginTop: 10,
- },
- title: {
- fontSize: 20,
- marginBottom: 10,
- fontWeight: "bold",
- alignSelf: "center",
- color: "#fff",
- },
- errorText: {
- color: "#ff4444",
- marginVertical: 10,
- },
- card: {
- backgroundColor: "#1F1F1F",
- borderRadius: 8,
- padding: 16,
- marginBottom: 10,
- },
- cardTitle: {
- fontSize: 16,
- fontWeight: "bold",
- color: "#fff",
- marginBottom: 4,
- },
- cardSubtitle: {
- fontSize: 14,
- color: "#ccc",
- marginBottom: 8,
- },
- cardRow: {
- flexDirection: "row",
- marginBottom: 4,
- alignItems: "center",
- },
- cardLabel: {
- marginRight: 4,
- color: "#ccc",
- fontSize: 14,
- },
- cardValue: {
- marginRight: 12,
- color: "#fff",
- fontSize: 14,
- },
- paginationContainer: {
- flexDirection: "row",
- justifyContent: "center",
- alignItems: "center",
- marginVertical: 10,
- },
- button: {
- backgroundColor: "#007AFF",
- paddingHorizontal: 10,
- paddingVertical: 6,
- borderRadius: 6,
- marginHorizontal: 5,
- },
- disabledButton: {
- backgroundColor: "#555",
- },
- buttonText: {
- color: "#fff",
- fontWeight: "600",
- },
- pageIndicator: {
- fontSize: 16,
- marginHorizontal: 10,
- color: "#fff",
- },
+ container: {
+ flex: 1,
+ paddingTop: 5,
+ paddingHorizontal: 16,
+ backgroundColor: '#121212',
+ },
+ scrollContainer: {
+ marginTop: 10,
+ },
+ title: {
+ fontSize: 20,
+ marginBottom: 10,
+ fontWeight: 'bold',
+ alignSelf: 'center',
+ color: '#fff',
+ },
+ errorText: {
+ color: '#ff4444',
+ marginVertical: 10,
+ },
+ noJobsText: {
+ color: '#fff',
+ textAlign: 'center',
+ marginTop: 20,
+ fontSize: 16,
+ },
+ card: {
+ backgroundColor: '#1F1F1F',
+ borderRadius: 8,
+ padding: 16,
+ marginBottom: 10,
+ },
+ cardTitle: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ color: '#fff',
+ marginBottom: 8,
+ },
+ cardRow: {
+ flexDirection: 'row',
+ marginBottom: 4,
+ alignItems: 'center',
+ flexWrap: 'wrap',
+ },
+ cardLabel: {
+ marginRight: 4,
+ color: '#ccc',
+ fontSize: 14,
+ },
+ cardValue: {
+ marginRight: 12,
+ color: '#fff',
+ fontSize: 14,
+ flex: 1,
+ },
+ paginationContainer: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginVertical: 10,
+ },
+ button: {
+ backgroundColor: '#007AFF',
+ paddingHorizontal: 10,
+ paddingVertical: 6,
+ borderRadius: 6,
+ marginHorizontal: 5,
+ },
+ disabledButton: {
+ backgroundColor: '#555',
+ },
+ buttonText: {
+ color: '#fff',
+ fontWeight: '600',
+ },
+ pageIndicator: {
+ fontSize: 16,
+ marginHorizontal: 10,
+ color: '#fff',
+ },
});
diff --git a/app/(tabs)/jobs.tsx.new b/app/(tabs)/jobs.tsx.new
new file mode 100644
index 0000000..e69de29
diff --git a/app/(tabs)/settings.tsx b/app/(tabs)/settings.tsx
index ac8d868..0d22deb 100644
--- a/app/(tabs)/settings.tsx
+++ b/app/(tabs)/settings.tsx
@@ -1,14 +1,18 @@
import { View, Text, StyleSheet } from "react-native";
import ClipboardDemo from "@/components/ClipboardDemo";
+import CustomHeader from "@/components/CustomHeader";
export default function SimplePage() {
return (
-
- My Header
-
- This is some example text to show a plain page component.
-
-
+
+
+
+ My Header
+
+ This is some example text to show a plain page component.
+
+
+
);
}
diff --git a/app/(tabs)/settings.tsx.new b/app/(tabs)/settings.tsx.new
new file mode 100644
index 0000000..e69de29
diff --git a/app/+not-found.tsx b/app/+not-found.tsx
index e137336..fe576c2 100644
--- a/app/+not-found.tsx
+++ b/app/+not-found.tsx
@@ -1,40 +1,40 @@
-import { Link, Stack } from "expo-router";
-import { StyleSheet } from "react-native";
+import { Link, Stack } from 'expo-router';
+import { StyleSheet } from 'react-native';
-import { Text, View } from "@/components/Themed";
+import { Text, View } from '@/components/Themed';
export default function NotFoundScreen() {
- return (
- <>
-
-
- This screen doesn't exist.
+ return (
+ <>
+
+
+ This screen doesn't exist.
-
- Go to home screen!
-
-
- >
- );
+
+ Go to home screen!
+
+
+ >
+ );
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- alignItems: "center",
- justifyContent: "center",
- padding: 20,
- },
- title: {
- fontSize: 20,
- fontWeight: "bold",
- },
- link: {
- marginTop: 15,
- paddingVertical: 15,
- },
- linkText: {
- fontSize: 14,
- color: "#2e78b7",
- },
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: 20,
+ },
+ title: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ },
+ link: {
+ marginTop: 15,
+ paddingVertical: 15,
+ },
+ linkText: {
+ fontSize: 14,
+ color: '#2e78b7',
+ },
});
diff --git a/app/login.tsx b/app/login.tsx
index 4b05b74..28e9cd8 100644
--- a/app/login.tsx
+++ b/app/login.tsx
@@ -1,64 +1,415 @@
-import { useEffect } from 'react';
-import { View, StyleSheet, Text } from 'react-native';
+import { useEffect, useRef, useState } from 'react';
+import { View, StyleSheet, Text, SafeAreaView, Image, ScrollView } from 'react-native';
import { GoogleSignin, GoogleSigninButton } from '@react-native-google-signin/google-signin';
import Constants from 'expo-constants';
import { useAuth } from '@/hooks/useAuth';
+import { notify } from '@/services/notifier.service';
export default function LoginScreen() {
- const { setUser } = useAuth();
+ // Add state for tracking rendering and errors
+ const [isRendering, setIsRendering] = useState(true);
+ const [renderError, setRenderError] = useState(null);
+ // Set up a basic UI first with minimal dependencies
useEffect(() => {
- const webClientId =
- Constants.expoConfig?.extra?.googleWebClientId ||
- // Expo public envs are embedded at bundle-time
- (process.env.EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID as string | undefined);
- if (!webClientId) {
- console.warn(
- 'Google Web Client ID is missing. Set GOOGLE_WEB_CLIENT_ID (app.config.js) or EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID in your .env/EAS env. Falling back to no offline access.'
- );
- // Configure without offline access to avoid runtime error
- GoogleSignin.configure({});
- return;
- }
- GoogleSignin.configure({
- webClientId,
- // offlineAccess allows getting a refresh token; requires a Server (Web) OAuth client ID
- offlineAccess: true,
- });
+ console.log('🔍 LoginScreen mounted');
+ setIsRendering(false);
+
+ return () => {
+ console.log('🔍 LoginScreen unmounted');
+ };
+ }, []);
+
+ // If we can't even render this basic UI, there's a serious problem elsewhere
+ if (isRendering) {
+ return (
+
+ Loading login screen...
+
+ );
+ }
+
+ if (renderError) {
+ return (
+
+ Error rendering login: {renderError}
+
+ );
+ }
+
+ // If we get here, we can try to render the full component
+ try {
+ return ;
+ } catch (err: any) {
+ console.error('❌ Error rendering LoginScreenContent:', err?.message || String(err));
+ return (
+
+ Failed to render login screen
+ {err?.message || String(err)}
+
+ );
+ }
+}
+
+// Separate the complex logic into its own component
+function LoginScreenContent() {
+ const { user, setUser } = useAuth();
+ // Flag to track if we've already configured GoogleSignin
+ const hasConfiguredRef = useRef(false);
+
+ useEffect(() => {
+ // Create an async function inside useEffect
+ const configureGoogleSignIn = async () => {
+ // Only configure once to prevent recursion
+ if (hasConfiguredRef.current) {
+ return;
+ }
+
+ hasConfiguredRef.current = true;
+
+ // Simple configuration with minimal logging to prevent recursion
+ try {
+ const webClientId =
+ Constants.expoConfig?.extra?.googleWebClientId ||
+ // Expo public envs are embedded at bundle-time
+ (process.env.EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID as string | undefined);
+
+ if (!webClientId) {
+ console.warn('🟡 Google Web Client ID is missing. Falling back to no offline access.');
+ GoogleSignin.configure({
+ scopes: ['profile', 'email'],
+ });
+ return;
+ }
+
+ console.log('🟢 Configuring Google Sign-In with client ID:', webClientId);
+
+ // Configure Google Sign-In
+ GoogleSignin.configure({
+ webClientId,
+ offlineAccess: true,
+ scopes: ['profile', 'email'],
+ });
+ } catch (error: any) {
+ console.error('❌ Error during Google Sign-In setup:', error?.message || String(error));
+ notify({
+ type: 'error',
+ title: 'Google Sign-In Setup Error',
+ message: error?.message || 'Failed to configure Google Sign-In',
+ });
+ }
+ };
+
+ // Call the async function
+ configureGoogleSignIn();
}, []);
const signIn = async () => {
try {
+ console.log('🟢 Starting Google sign in process...');
+
+ // Check if Google Play Services are available
+ console.log('🟢 Checking Google Play Services...');
await GoogleSignin.hasPlayServices({ showPlayServicesUpdateDialog: true });
- const result = await GoogleSignin.signIn();
-
- // @react-native-google-signin/google-signin returns an object with idToken and user
- if (result?.idToken) {
- setUser({ idToken: result.idToken, user: result.user });
- fetch('https://new.codebuilder.org/api/auth/google', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ idToken: result.idToken }),
- }).catch((e) => console.error('Auth callback error:', e));
+ console.log('✅ Google Play Services available');
+
+ // Attempt to sign in
+ console.log('🟢 Initiating Google Sign-In...');
+
+ // Perform the sign-in
+ const signInResult = await GoogleSignin.signIn();
+
+ // The structure from Expo's implementation is different - data is nested
+ // Expo adds a wrapper with { type: 'success', data: { actual response } }
+ console.log('📋 Google Sign-In raw result:', JSON.stringify(signInResult, null, 2));
+
+ // Extract the data based on the structure
+ let userData;
+ let idToken;
+
+ if (signInResult?.type === 'success' && signInResult?.data) {
+ // Expo structure
+ userData = signInResult.data.user;
+ idToken = signInResult.data.idToken;
} else {
- console.warn('Google sign in did not return an idToken.');
+ // Regular structure
+ userData = signInResult?.user;
+ idToken = signInResult?.idToken;
+ }
+
+ if (userData && idToken) {
+ console.log('✅ Google Sign-In successful:', userData.email);
+ console.log('✅ ID Token received, setting user and calling API');
+
+ // Set the initial user in context (we'll update with tokens after API call)
+ setUser({
+ idToken,
+ user: {
+ id: userData.id,
+ name: userData.name,
+ email: userData.email,
+ photo: userData.photo,
+ familyName: userData.familyName,
+ givenName: userData.givenName,
+ },
+ });
+
+ // Make the API call
+ console.log('🟢 Making network request to auth API...');
+ try {
+ const response = await fetch('https://api.codebuilder.org/auth/google', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ idToken }),
+ });
+
+ console.log('✅ Auth API response received:', response.status);
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`API error (${response.status}): ${errorText || 'No error details'}`);
+ }
+
+ let responseData;
+ const contentType = response.headers.get('content-type');
+ if (contentType && contentType.includes('application/json')) {
+ responseData = await response.json();
+ } else {
+ const text = await response.text();
+ try {
+ // Try to parse text as JSON in case content-type is incorrect
+ responseData = JSON.parse(text);
+ } catch (e) {
+ console.log('✅ Auth API response (text):', text || '(empty response)');
+ throw new Error('API response was not valid JSON');
+ }
+ }
+
+ console.log('✅ Auth API response processed');
+
+ if (responseData.accessToken && responseData.refreshToken) {
+ // Update user with tokens
+ setUser((prevUser) => ({
+ ...prevUser!,
+ accessToken: responseData.accessToken,
+ refreshToken: responseData.refreshToken,
+ }));
+
+ // Show success notification
+ notify({
+ type: 'success',
+ title: 'Sign-In Successful',
+ message: `Welcome, ${userData.name || userData.email}!`,
+ });
+ } else {
+ // Tokens missing from API response
+ throw new Error('API response missing access or refresh tokens');
+ }
+ } catch (apiError: any) {
+ console.error('🔴 Auth callback error:', apiError?.message || String(apiError));
+ notify({
+ type: 'error',
+ title: 'Authentication Error',
+ message: apiError?.message || 'Failed to authenticate with server',
+ });
+ }
+ } else {
+ console.warn('🟡 Google sign in did not return expected user data or token.');
+ notify({
+ type: 'error',
+ title: 'Sign-In Failed',
+ message: 'Google sign-in did not return expected user data or token',
+ });
}
} catch (e: any) {
- // Log richer error info for troubleshooting (code, description)
+ // Log error info for troubleshooting
const code = e?.code || e?.statusCodes || 'UNKNOWN';
- console.error('Google sign in error:', code, e?.message || e);
+ const message = e?.message || 'No error message available';
+ const statusCode = e?.statusCode || 'N/A';
+
+ console.error('🔴 Google sign in error:', '\nError code:', code, '\nStatus code:', statusCode, '\nMessage:', message);
+
+ notify({
+ type: 'error',
+ title: 'Google Sign-In Failed',
+ message: message || 'Failed to sign in with Google',
+ });
+ }
+ };
+
+ const signOut = async () => {
+ try {
+ console.log('🔄 Signing out from Google...');
+ await GoogleSignin.signOut();
+ console.log('✅ Signed out successfully');
+ setUser(null);
+ notify({
+ type: 'info',
+ title: 'Signed Out',
+ message: 'You have been successfully signed out',
+ });
+ } catch (e: any) {
+ console.error('❌ Sign out error:', e?.message || String(e));
+ notify({
+ type: 'error',
+ title: 'Sign-Out Error',
+ message: e?.message || 'Failed to sign out',
+ });
+ }
+ };
+
+ const revokeAccess = async () => {
+ try {
+ console.log('⚠️ Revoking Google access...');
+ await GoogleSignin.revokeAccess();
+ await GoogleSignin.signOut();
+ console.log('✅ Access revoked successfully');
+ setUser(null);
+ notify({
+ type: 'info',
+ title: 'Access Revoked',
+ message: 'Your Google access has been revoked',
+ });
+ } catch (e: any) {
+ console.error('❌ Revoke access error:', e?.message || String(e));
+ notify({
+ type: 'error',
+ title: 'Revoke Access Error',
+ message: e?.message || 'Failed to revoke access',
+ });
}
};
+ // Show different UI based on authentication state
+ if (!user) {
+ return (
+
+ Login
+
+
+ );
+ }
+
+ // User is logged in
return (
-
- Login
-
-
+
+ Account Information
+
+ {user.user.photo && }
+
+
+ Name:
+ {user.user.name || 'N/A'}
+
+ Email:
+ {user.user.email}
+
+ Status:
+ {user.accessToken ? '✓ Authenticated' : '⚠️ Partial Auth'}
+
+
+
+ ID Token:
+
+ {user.idToken.substring(0, 15)}...
+
+
+ {user.accessToken && (
+ <>
+ Access Token:
+
+ {user.accessToken.substring(0, 15)}...
+
+ >
+ )}
+
+ {user.refreshToken && (
+ <>
+ Refresh Token:
+
+ {user.refreshToken.substring(0, 15)}...
+
+ >
+ )}
+
+
+
+
+ Sign Out
+
+
+
+ Revoke Access
+
+
+
);
}
const styles = StyleSheet.create({
- container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
+ container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 16 },
title: { fontSize: 24, fontWeight: '600', marginBottom: 16 },
+ profileImage: {
+ width: 100,
+ height: 100,
+ borderRadius: 50,
+ marginBottom: 20,
+ },
+ infoContainer: {
+ width: '100%',
+ backgroundColor: '#f5f5f5',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 20,
+ },
+ label: {
+ fontWeight: '600',
+ marginBottom: 4,
+ color: '#555',
+ },
+ value: {
+ marginBottom: 16,
+ fontSize: 16,
+ },
+ statusBadge: {
+ color: '#2e7d32',
+ fontWeight: '600',
+ },
+ tokenInfo: {
+ width: '100%',
+ backgroundColor: '#e8f5e9',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 20,
+ },
+ tokenLabel: {
+ fontWeight: '600',
+ marginBottom: 4,
+ color: '#2e7d32',
+ },
+ tokenValue: {
+ marginBottom: 16,
+ fontSize: 14,
+ fontFamily: 'monospace',
+ color: '#1b5e20',
+ },
+ buttonContainer: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ width: '100%',
+ marginTop: 8,
+ },
+ signOutButton: {
+ color: '#2196f3',
+ marginRight: 16,
+ textDecorationLine: 'underline',
+ fontSize: 16,
+ padding: 8,
+ },
+ revokeButton: {
+ color: '#f44336',
+ textDecorationLine: 'underline',
+ fontSize: 16,
+ padding: 8,
+ },
});
diff --git a/components/CustomHeader.tsx b/components/CustomHeader.tsx
new file mode 100644
index 0000000..c4829f7
--- /dev/null
+++ b/components/CustomHeader.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import { View, Text, StyleSheet, Pressable } from 'react-native';
+import { useRouter, Link } from 'expo-router';
+import FontAwesome from '@expo/vector-icons/FontAwesome';
+import Colors from '@/constants/Colors';
+import { useColorScheme } from '@/hooks/useColorScheme';
+
+interface CustomHeaderProps {
+ title: string;
+ showBackButton?: boolean;
+ showModalButton?: boolean;
+}
+
+export default function CustomHeader({ title, showBackButton = false, showModalButton = false }: CustomHeaderProps) {
+ const router = useRouter();
+ const colorScheme = useColorScheme();
+ const textColor = Colors[colorScheme ?? 'light'].text;
+
+ return (
+
+ {showBackButton && (
+ router.back()}>
+
+
+ )}
+
+ {title}
+
+ {showModalButton && (
+
+ {({ pressed }) => }
+
+ )}
+
+ {!showModalButton && }
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ height: 60,
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingHorizontal: 15,
+ backgroundColor: '#121212',
+ borderBottomWidth: 1,
+ borderBottomColor: '#333',
+ paddingTop: 5,
+ marginBottom: 5,
+ },
+ backButton: {
+ width: 40,
+ height: 40,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ title: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ flex: 1,
+ textAlign: 'center',
+ },
+ rightButton: {
+ width: 40,
+ height: 40,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ placeholderRight: {
+ width: 40,
+ },
+});
diff --git a/components/LogViewer.tsx b/components/LogViewer.tsx
index 6efd7bf..5a43a0c 100644
--- a/components/LogViewer.tsx
+++ b/components/LogViewer.tsx
@@ -1,34 +1,109 @@
import React, { useEffect, useRef, useState } from 'react';
-import { ScrollView, Text, View, Button } from 'react-native';
+import { ScrollView, Text, View, Button, StyleSheet } from 'react-native';
import { ConsoleView } from 'react-native-console-view';
+// Log level types and styling properties
+type LogLevel = 'log' | 'error' | 'warn';
+
+// Emoji indicators for each log level
+const emojiForLevel: Record = {
+ log: '🟢',
+ error: '🔴',
+ warn: '🟡',
+};
+
+// Colors for each log level
+const colorForLevel: Record = {
+ log: '#2ecc40', // Green
+ error: '#e74c3c', // Red
+ warn: '#f1c40f', // Yellow
+};
+
+interface LogEntry {
+ level: LogLevel;
+ message: string;
+ timestamp: string;
+}
+
const LogViewer = () => {
- const [logs, setLogs] = useState([]);
+ const [logs, setLogs] = useState([]);
+ const scrollViewRef = useRef(null);
+
+ // Flag to prevent recursive logging
+ const isLoggingRef = useRef(false);
- const flushingRef = useRef(false);
useEffect(() => {
const originalLog = console.log;
const originalError = console.error;
const originalWarn = console.warn;
- const enqueue = (level: 'log' | 'error' | 'warn', args: any[]) => {
- // Defer state update to microtask to avoid setState during another component's render
- Promise.resolve().then(() => {
- setLogs((prev) => [...prev, `[${level.toUpperCase()}] ${args.map(String).join(' ')}`]);
- });
+ const enqueue = (level: LogLevel, args: any[]) => {
+ // Prevent recursive logging
+ if (isLoggingRef.current) {
+ return;
+ }
+
+ try {
+ isLoggingRef.current = true;
+
+ // Format timestamp for better readability
+ const now = new Date();
+ const timestamp = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now
+ .getSeconds()
+ .toString()
+ .padStart(2, '0')}.${now.getMilliseconds().toString().padStart(3, '0')}`;
+
+ // Safely convert args to strings
+ let messageStr = '';
+ try {
+ messageStr = args
+ .map((arg) => {
+ if (typeof arg === 'string') return arg;
+ if (arg instanceof Error) return arg.toString();
+ try {
+ return JSON.stringify(arg);
+ } catch (e) {
+ return String(arg);
+ }
+ })
+ .join(' ');
+ } catch (err) {
+ messageStr = 'Error stringifying log message';
+ }
+
+ // Defer state update to microtask to avoid setState during another component's render
+ Promise.resolve().then(() => {
+ setLogs((prev) => {
+ // Add new log to the beginning (newest first)
+ const newLogs = [
+ {
+ level,
+ message: messageStr,
+ timestamp,
+ },
+ ...prev,
+ ];
+
+ // Keep only the latest 100 logs to prevent memory issues
+ return newLogs.slice(0, 100);
+ });
+ });
+ } finally {
+ isLoggingRef.current = false;
+ }
};
console.log = (...args) => {
- enqueue('log', args);
originalLog(...args);
+ enqueue('log', args);
};
console.error = (...args) => {
- enqueue('error', args);
originalError(...args);
+ enqueue('error', args);
};
console.warn = (...args) => {
- enqueue('warn', args);
originalWarn(...args);
+ enqueue('warn', args);
};
return () => {
@@ -38,29 +113,84 @@ const LogViewer = () => {
};
}, []);
+ // We don't need auto-scroll since newest logs are at the top
+ useEffect(() => {
+ if (scrollViewRef.current && logs.length > 0) {
+ scrollViewRef.current?.scrollTo({ y: 0, animated: true });
+ }
+ }, [logs.length]);
+
return (
-
+
-
+
{logs.map((log, index) => (
-
- {log}
-
+
+
+ {emojiForLevel[log.level]} [{log.timestamp}] [{log.level.toUpperCase()}]
+
+ {log.message}
+ {index < logs.length - 1 && }
+
))}
-
);
};
+const styles = StyleSheet.create({
+ container: {
+ padding: 10,
+ backgroundColor: '#121212',
+ borderRadius: 15,
+ borderWidth: 2,
+ borderColor: '#3498db',
+ margin: 10,
+ height: 400, // Fixed height instead of flex: 1
+ overflow: 'hidden', // Prevent content from overflowing
+ },
+ scrollView: {
+ marginTop: 10,
+ height: 300, // Fixed height to ensure scrollability
+ flexGrow: 0, // Prevent expanding
+ },
+ scrollContent: {
+ paddingBottom: 10,
+ },
+ logItem: {
+ marginBottom: 8,
+ paddingHorizontal: 8,
+ paddingVertical: 4,
+ },
+ logHeader: {
+ fontSize: 13,
+ fontWeight: 'bold',
+ marginBottom: 4,
+ },
+ logMessage: {
+ color: 'white',
+ fontSize: 12,
+ marginLeft: 8,
+ },
+ separator: {
+ borderBottomWidth: 1,
+ borderBottomColor: '#444',
+ marginTop: 8,
+ marginBottom: 8,
+ },
+ buttonContainer: {
+ marginTop: 10,
+ },
+});
+
export default LogViewer;
diff --git a/google-services.json b/google-services.json
index 5ad11ed..9cb7908 100644
--- a/google-services.json
+++ b/google-services.json
@@ -14,7 +14,7 @@
},
"oauth_client": [
{
- "client_id": "906784991953-aoq1tgiac6ge9aqv4kguai26265393h9.apps.googleusercontent.com",
+ "client_id": "982053776531-0gqv0n84kjid9vtu00iq9drjvf9dvjhn.apps.googleusercontent.com",
"client_type": 3
}
],
@@ -27,7 +27,7 @@
"appinvite_service": {
"other_platform_oauth_client": [
{
- "client_id": "906784991953-aoq1tgiac6ge9aqv4kguai26265393h9.apps.googleusercontent.com",
+ "client_id": "982053776531-0gqv0n84kjid9vtu00iq9drjvf9dvjhn.apps.googleusercontent.com",
"client_type": 3
}
]
diff --git a/hooks/useAuth.ts b/hooks/useAuth.ts
index 67710ca..aab7fbd 100644
--- a/hooks/useAuth.ts
+++ b/hooks/useAuth.ts
@@ -1,28 +1,30 @@
// hooks/useAuth.ts
-import { useContext } from "react";
-import { AuthContext } from "@/providers/AuthProvider";
+import { useContext } from 'react';
+import { AuthContext } from '@/providers/AuthProvider';
export interface AuthUser {
- idToken: string;
- user: {
- id: string;
- name: string | null;
- email: string;
- photo: string | null;
- familyName: string | null;
- givenName: string | null;
- };
+ idToken: string;
+ accessToken?: string;
+ refreshToken?: string;
+ user: {
+ id: string;
+ name: string | null;
+ email: string;
+ photo: string | null;
+ familyName: string | null;
+ givenName: string | null;
+ };
}
export type AuthContextType = {
- user: AuthUser | null;
- setUser: React.Dispatch>;
+ user: AuthUser | null;
+ setUser: React.Dispatch>;
};
export function useAuth(): AuthContextType {
- const ctx = useContext(AuthContext);
- if (!ctx) {
- throw new Error("useAuth must be used within ");
- }
- return ctx;
+ const ctx = useContext(AuthContext);
+ if (!ctx) {
+ throw new Error('useAuth must be used within ');
+ }
+ return ctx;
}
diff --git a/scripts/buildScripts.js b/scripts/buildScripts.js
index aacbb1b..7ba9d48 100644
--- a/scripts/buildScripts.js
+++ b/scripts/buildScripts.js
@@ -3,57 +3,53 @@ const path = require("path");
const { execSync, execFileSync } = require("child_process");
// Configuration file path
-const CONFIG_FILE = path.join(__dirname, "build-config.json");
+const CONFIG_FILE = path.join(__dirname, 'build-config.json');
// Default configuration
const DEFAULT_CONFIG = {
- scripts: [
- "setupBatteryOptimizations.js",
- "notificationSounds.ts",
- "setupExceptionHandler.js",
- ],
- // Add enabled: false to disable a script without removing it from the list
- // Example: { path: "scriptName.js", enabled: false }
+ scripts: ['setupBatteryOptimizations.js', 'notificationSounds.ts', 'setupExceptionHandler.js'],
+ // Add enabled: false to disable a script without removing it from the list
+ // Example: { path: "scriptName.js", enabled: false }
};
// Create default config if it doesn't exist
function ensureConfigExists() {
- if (!fs.existsSync(CONFIG_FILE)) {
- fs.writeFileSync(CONFIG_FILE, JSON.stringify(DEFAULT_CONFIG, null, 2));
- console.log(`Created default build config at ${CONFIG_FILE}`);
- }
+ if (!fs.existsSync(CONFIG_FILE)) {
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(DEFAULT_CONFIG, null, 2));
+ console.log(`Created default build config at ${CONFIG_FILE}`);
+ }
}
// Get the list of scripts to run
function getScriptsToRun() {
- ensureConfigExists();
- const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
- return config.scripts
- .filter((script) => {
- if (typeof script === "string") return true;
- return script.enabled !== false;
- })
- .map((script) => {
- if (typeof script === "string") return script;
- return script.path;
- });
+ ensureConfigExists();
+ const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
+ return config.scripts
+ .filter((script) => {
+ if (typeof script === 'string') return true;
+ return script.enabled !== false;
+ })
+ .map((script) => {
+ if (typeof script === 'string') return script;
+ return script.path;
+ });
}
// Run all enabled scripts
function runBuildScripts() {
- console.log("🚀 Running build scripts...");
+ console.log('🚀 Running build scripts...');
- const scripts = getScriptsToRun();
+ const scripts = getScriptsToRun();
- scripts.forEach((scriptPath) => {
- const fullPath = path.join(__dirname, scriptPath);
+ scripts.forEach((scriptPath) => {
+ const fullPath = path.join(__dirname, scriptPath);
- if (!fs.existsSync(fullPath)) {
- console.error(`❌ Script not found: ${fullPath}`);
- return;
- }
+ if (!fs.existsSync(fullPath)) {
+ console.error(`❌ Script not found: ${fullPath}`);
+ return;
+ }
- console.log(`▶️ Running: ${scriptPath}`);
+ console.log(`▶️ Running: ${scriptPath}`);
try {
if (scriptPath.endsWith(".ts")) {
@@ -69,12 +65,12 @@ function runBuildScripts() {
}
});
- console.log("✨ Build scripts completed");
+ console.log('✨ Build scripts completed');
}
// Run the function if this is the main module
if (require.main === module) {
- runBuildScripts();
+ runBuildScripts();
}
module.exports = runBuildScripts;
diff --git a/services/errorReporting.service.ts b/services/errorReporting.service.ts
index baee822..d41e593 100644
--- a/services/errorReporting.service.ts
+++ b/services/errorReporting.service.ts
@@ -1,73 +1,131 @@
-import { Platform } from "react-native";
-import { ErrorInfo } from "react";
+import { Platform } from 'react-native';
+import { ErrorInfo } from 'react';
export interface ReportOptions {
- isFatal?: boolean;
- errorInfo?: ErrorInfo;
+ isFatal?: boolean;
+ errorInfo?: ErrorInfo;
}
export interface ErrorReport {
- name: string;
- cause?: string;
- message: string;
- stack?: string;
- timestamp: string;
- platform: string;
- options?: ReportOptions;
+ name: string;
+ cause?: string;
+ message: string;
+ stack?: string;
+ timestamp: string;
+ platform: string;
+ options?: ReportOptions;
}
-const ERROR_REPORTING_ENDPOINT = "https://new.codebuilder.org/api/errors";
+const ERROR_REPORTING_ENDPOINT = 'https://new.codebuilder.org/api/errors';
+
+// Circuit breaker implementation
+const circuitBreaker = {
+ failureCount: 0,
+ lastFailureTime: 0,
+ isOpen: false,
+ failureThreshold: 3,
+ resetTimeout: 60000, // 1 minute in ms
+ baseDelay: 5000, // 5 seconds initial delay
+ maxDelay: 3600000, // 1 hour maximum delay
+ shouldPreventApiCall(): boolean {
+ if (this.isOpen) {
+ const now = Date.now();
+ const timeSinceLastFailure = now - this.lastFailureTime;
+ const currentBackoff = Math.min(this.baseDelay * Math.pow(2, this.failureCount - 1), this.maxDelay);
+ if (timeSinceLastFailure > currentBackoff) {
+ // Allow one test request
+ return false;
+ }
+ return true;
+ }
+ return false;
+ },
+ recordSuccess(): void {
+ if (this.failureCount > 0) {
+ console.log('Error reporting API is back online');
+ }
+ this.failureCount = 0;
+ this.isOpen = false;
+ },
+ recordFailure(): void {
+ const now = Date.now();
+ this.failureCount++;
+ this.lastFailureTime = now;
+ if (this.failureCount >= this.failureThreshold) {
+ this.isOpen = true;
+ const backoffTime = Math.min(this.baseDelay * Math.pow(2, this.failureCount - 1), this.maxDelay);
+ console.log(`Circuit opened after ${this.failureCount} failures. Will retry in ${backoffTime / 1000}s`);
+ }
+ },
+};
+
+// Flag to prevent recursive error reporting
+let isReportingError = false;
+// Count nested error reporting attempts to detect potential infinite loops
+let nestedReportCount = 0;
+const MAX_NESTED_REPORTS = 3;
// Wrap fetch in a safe function so it never throws
-export const safeReport = async (
- error: Error,
- options?: ReportOptions
-): Promise => {
- // Build normalized report
- const report: ErrorReport = {
- name: error.name,
- message: error.message || String(error),
- stack: error.stack || undefined,
- cause: error.cause ? String(error.cause) : undefined,
- timestamp: new Date().toISOString(),
- platform: Platform.OS,
- options,
- };
- try {
- const response = await fetch(ERROR_REPORTING_ENDPOINT, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(report),
- });
+export const safeReport = async (error: Error, options?: ReportOptions): Promise => {
+ // Prevent recursive error reporting
+ if (isReportingError) {
+ nestedReportCount++;
+ console.log(`Already reporting an error, skipping to prevent recursion (depth: ${nestedReportCount})`);
- if (!response.ok) {
- console.error(
- "Failed to send error report:",
- `Status: ${response.status}`,
- `StatusText: ${response.statusText}`,
- response,
- report
- );
+ // If we detect too many nested error reports, something is wrong
+ if (nestedReportCount >= MAX_NESTED_REPORTS) {
+ console.log('Too many nested error reports - likely an infinite loop. Breaking the cycle.');
+ return;
+ }
+ return;
+ }
+ // Check if circuit breaker is open
+ if (circuitBreaker.shouldPreventApiCall()) {
+ console.log('Error reporting circuit is open, skipping API call');
+ return;
+ }
+
+ // Build normalized report
+ const report: ErrorReport = {
+ name: error.name,
+ message: error.message || String(error),
+ stack: error.stack || undefined,
+ cause: error.cause ? String(error.cause) : undefined,
+ timestamp: new Date().toISOString(),
+ platform: Platform.OS,
+ options,
+ };
+
+ try {
+ isReportingError = true;
+ const response = await fetch(ERROR_REPORTING_ENDPOINT, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(report),
+ });
+
+ if (!response.ok) {
+ // Use console.log to avoid triggering global error handler
+ console.log('Failed to send error report:', `Status: ${response.status}`, `StatusText: ${response.statusText}`, response, report);
+ circuitBreaker.recordFailure();
+ } else {
+ circuitBreaker.recordSuccess();
+ }
+ } catch (e) {
+ // Use console.log to avoid triggering global error handler
+ console.log('Error sending error report (swallowed):', e);
+ circuitBreaker.recordFailure();
+ } finally {
+ isReportingError = false;
+ nestedReportCount = 0; // Reset the nested count when we're done
}
- } catch (e) {
- console.error("Error sending error report (swallowed):", e);
- }
};
// Original API for backward compatibility
-export const reportError = (
- maybeError: unknown,
- options?: ReportOptions
-): void => {
- const error =
- maybeError instanceof Error
- ? maybeError
- : new Error(
- typeof maybeError === "string"
- ? maybeError
- : JSON.stringify(maybeError) || "Unknown non-Error thrown"
- );
- // Fire-and-forget
- void safeReport(error, options);
+
+export const reportError = (maybeError: unknown, options?: ReportOptions): void => {
+ const error = maybeError instanceof Error ? maybeError : new Error(typeof maybeError === 'string' ? maybeError : JSON.stringify(maybeError) || 'Unknown non-Error thrown');
+ // Fire-and-forget
+ void safeReport(error, options);
};
diff --git a/utils/globalErrorhandler.ts b/utils/globalErrorhandler.ts
index f35e269..fcd2617 100644
--- a/utils/globalErrorhandler.ts
+++ b/utils/globalErrorhandler.ts
@@ -5,6 +5,8 @@ import { notifyError } from '@/services/notifier.service';
// JavaScript Error Handler
export const setupGlobalErrorHandlers = () => {
setJSExceptionHandler((error, isFatal) => {
+ // Always print error to console for debugging
+ console.error('Global JS Exception:', error, 'isFatal:', isFatal);
// Report the error using your actual reportError function
reportError(error, { isFatal });
@@ -15,6 +17,8 @@ export const setupGlobalErrorHandlers = () => {
// Native Exception Handler
setNativeExceptionHandler(
(exceptionString) => {
+ // Always print native exception to console
+ console.error('Global Native Exception:', exceptionString);
// This is called when native code throws an exception
const error = new Error(`Native Exception: ${exceptionString}`);
reportError(error, { isFatal: true });
diff --git a/utils/tasks.utils.ts b/utils/tasks.utils.ts
index 0e077d4..0553346 100644
--- a/utils/tasks.utils.ts
+++ b/utils/tasks.utils.ts
@@ -8,7 +8,7 @@ TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
try {
console.log('----------------------- Background fetch task started ---------------------');
// Perform your API request
- const response = await fetch('https://new.codebuilder.org/api/jobs');
+ const response = await fetch('https://api.codebuilder.org/jobs/fetch');
const contentType = response.headers.get('content-type') || '';
if (!response.ok) {