fix: tsconfig ignoreDeprecations and GeofenceStatus component - use GYM_LOCATION constant

This commit is contained in:
Aleksandar 2025-12-15 12:19:27 +01:00
parent 2f79d1547f
commit 231ad44cc4
6 changed files with 287 additions and 1 deletions

View File

@ -28,6 +28,7 @@
"expo-haptics": "^15.0.7",
"expo-linear-gradient": "~15.0.7",
"expo-linking": "~8.0.0",
"expo-location": "~17.0.1",
"expo-notifications": "~0.32.0",
"expo-router": "~6.0.14",
"expo-secure-store": "~15.0.7",
@ -7329,6 +7330,15 @@
"react-native": "*"
}
},
"node_modules/expo-location": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/expo-location/-/expo-location-17.0.1.tgz",
"integrity": "sha512-m+OzotzlAXO3ZZ1uqW5GC25nXW868zN+ROyBA1V4VF6jGay1ZEs4URPglCVUDzZby2F5wt24cMzqDKw2IX6nRw==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-modules-autolinking": {
"version": "3.0.22",
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.22.tgz",

View File

@ -34,6 +34,7 @@
"expo-haptics": "^15.0.7",
"expo-linear-gradient": "~15.0.7",
"expo-linking": "~8.0.0",
"expo-location": "~17.0.1",
"expo-notifications": "~0.32.0",
"expo-router": "~6.0.14",
"expo-secure-store": "~15.0.7",

View File

@ -6,6 +6,7 @@ import { Ionicons } from '@expo/vector-icons'
import { attendanceApi, Attendance } from '../../api/attendance'
import { theme } from '../../styles/theme'
import { Animated } from 'react-native'
import { GeofenceStatus } from '../../components/GeofenceStatus'
export default function AttendanceScreen() {
const { getToken, userId } = useAuth()
@ -111,6 +112,9 @@ export default function AttendanceScreen() {
<Text style={styles.subtitle}>Track your gym visits</Text>
</LinearGradient>
{/* Geofencing Status Card */}
<GeofenceStatus />
<View style={styles.actionContainer}>
{activeCheckIn ? (
<LinearGradient

View File

@ -0,0 +1,198 @@
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator } from 'react-native'
import { useState, useEffect } from 'react'
import { LinearGradient } from 'expo-linear-gradient'
import { Ionicons } from '@expo/vector-icons'
import * as Location from 'expo-location'
import { theme } from '../styles/theme'
import { isWithinGeofence, calculateDistance, getFormattedDistance, GYM_LOCATION } from '../services/geofencing'
export interface GeofenceStatusProps {
onStatusChange?: (isInside: boolean) => void
}
export const GeofenceStatus = ({ onStatusChange }: GeofenceStatusProps) => {
const [loading, setLoading] = useState(false)
const [isInside, setIsInside] = useState(false)
const [distance, setDistance] = useState<string>('--')
const [permission, setPermission] = useState<string>('undetermined')
const [lastCheck, setLastCheck] = useState<Date | null>(null)
const checkGeofence = async () => {
try {
setLoading(true)
// Request location permission
const { status } = await Location.requestForegroundPermissionsAsync()
setPermission(status)
if (status !== 'granted') {
setLoading(false)
return
}
// Get current location
const location = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.Balanced,
})
const { latitude, longitude } = location.coords
// Check if within geofence
const within = isWithinGeofence(latitude, longitude, 500)
setIsInside(within)
onStatusChange?.(within)
// Calculate distance
const dist = calculateDistance(
latitude,
longitude,
GYM_LOCATION.latitude,
GYM_LOCATION.longitude
)
setDistance(getFormattedDistance(dist))
setLastCheck(new Date())
} catch (error) {
console.error('Geofence check error:', error)
} finally {
setLoading(false)
}
}
useEffect(() => {
checkGeofence()
// Auto-check every 30 seconds
const interval = setInterval(checkGeofence, 30000)
return () => clearInterval(interval)
}, [])
return (
<View style={styles.container}>
<LinearGradient
colors={
isInside
? ['#10b981', '#6ee7b7', '#a7f3d0']
: ['#64748b', '#94a3b8', '#cbd5e1']
}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.card}
>
<View style={styles.header}>
<View style={styles.titleRow}>
<Ionicons
name={isInside ? 'location-sharp' : 'location'}
size={24}
color="#fff"
/>
<Text style={styles.title}>
{isInside ? 'At Gym' : 'Away from Gym'}
</Text>
</View>
<TouchableOpacity
onPress={checkGeofence}
disabled={loading}
style={styles.refreshButton}
>
{loading ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Ionicons name="refresh" size={20} color="#fff" />
)}
</TouchableOpacity>
</View>
<View style={styles.content}>
<View style={styles.stat}>
<Text style={styles.statLabel}>Distance</Text>
<Text style={styles.statValue}>{distance}</Text>
</View>
{lastCheck && (
<Text style={styles.lastCheck}>
Last check: {lastCheck.toLocaleTimeString()}
</Text>
)}
</View>
{permission !== 'granted' && (
<View style={styles.permissionAlert}>
<Ionicons name="alert-circle" size={16} color="#fff" />
<Text style={styles.permissionText}>
Location permission required
</Text>
</View>
)}
</LinearGradient>
</View>
)
}
const styles = StyleSheet.create({
container: {
paddingHorizontal: 16,
paddingVertical: 12,
},
card: {
borderRadius: 16,
padding: 16,
overflow: 'hidden',
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
},
titleRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
title: {
fontSize: 18,
fontWeight: '600',
color: '#fff',
},
refreshButton: {
padding: 8,
borderRadius: 8,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
},
content: {
gap: 12,
},
stat: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
statLabel: {
fontSize: 14,
color: 'rgba(255, 255, 255, 0.8)',
fontWeight: '500',
},
statValue: {
fontSize: 16,
fontWeight: '600',
color: '#fff',
},
lastCheck: {
fontSize: 12,
color: 'rgba(255, 255, 255, 0.7)',
marginTop: 4,
},
permissionAlert: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
marginTop: 12,
paddingTop: 12,
borderTopWidth: 1,
borderTopColor: 'rgba(255, 255, 255, 0.3)',
},
permissionText: {
fontSize: 12,
color: '#fff',
fontWeight: '500',
},
})

View File

@ -0,0 +1,74 @@
/**
* Geofencing Service
* Handles gym location validation and distance calculations
*/
// Gym location coordinates (example: gym location)
export const GYM_LOCATION = {
latitude: 37.7749,
longitude: -122.4194,
};
// Geofence radius in meters (500m default)
export const GEOFENCE_RADIUS_METERS = 500;
/**
* Calculate distance between two coordinates using Haversine formula
* @param lat1 Starting latitude
* @param lon1 Starting longitude
* @param lat2 Ending latitude
* @param lon2 Ending longitude
* @returns Distance in meters
*/
export function calculateDistance(
lat1: number,
lon1: number,
lat2: number,
lon2: number
): number {
const R = 6371000; // Earth's radius in meters
const φ1 = (lat1 * Math.PI) / 180;
const φ2 = (lat2 * Math.PI) / 180;
const Δφ = ((lat2 - lat1) * Math.PI) / 180;
const Δλ = ((lon2 - lon1) * Math.PI) / 180;
const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c; // Distance in meters
}
/**
* Check if a location is within the geofence
* @param userLat User's latitude
* @param userLon User's longitude
* @param radius Geofence radius in meters (defaults to GEOFENCE_RADIUS_METERS)
* @returns Boolean indicating if user is within geofence
*/
export function isWithinGeofence(
userLat: number,
userLon: number,
radius: number = GEOFENCE_RADIUS_METERS
): boolean {
const distance = calculateDistance(
userLat,
userLon,
GYM_LOCATION.latitude,
GYM_LOCATION.longitude
);
return distance <= radius;
}
/**
* Get formatted distance string
* @param meters Distance in meters
* @returns Formatted distance string
*/
export function getFormattedDistance(meters: number): string {
if (meters < 1000) {
return `${Math.round(meters)} m`;
}
return `${(meters / 1000).toFixed(2)} km`;
}

View File

@ -12,7 +12,6 @@
"noEmit": true,
"strict": true,
"target": "esnext",
"ignoreDeprecations": "6.0",
"baseUrl": ".",
"paths": {
"@/*": [