pedometar

problem with expogo, should be sorted before build
This commit is contained in:
dimitar committed 2025-11-28 18:08:52 +01:00
1 parent b0d751540e
commit 24ed0b6190
16 files changed
+1676 -64

No files matched your search

+31 -5
View File
@@ -14,13 +14,20 @@
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"permissions": [
"android.permission.ACTIVITY_RECOGNITION"
],
"package": "com.anonymous.fitai"
},
"ios": {
"supportsTablet": true,
"infoPlist": {
"NSMotionUsageDescription": "This app uses the pedometer to track your steps."
}
},
"web": {
@@ -28,8 +35,27 @@
},
"plugins": [
"expo-router",
"expo-font"
"expo-font",
[
"react-native-health-connect",
{
"permissions": [
{
"accessType": "read",
"recordType": "Steps"
},
{
"accessType": "read",
"recordType": "TotalCaloriesBurned"
},
{
"accessType": "read",
"recordType": "ExerciseSession"
}
]
}
]
],
"scheme": "fitai"
}
}
}
+115
View File
@@ -22,6 +22,7 @@
"expo-camera": "~17.0.0",
"expo-constants": "^18.0.10",
"expo-crypto": "^15.0.7",
"expo-dev-client": "^6.0.18",
"expo-font": "~14.0.9",
"expo-haptics": "^15.0.7",
"expo-linear-gradient": "~15.0.7",
@@ -29,12 +30,14 @@
"expo-notifications": "~0.32.0",
"expo-router": "~6.0.14",
"expo-secure-store": "~15.0.7",
"expo-sensors": "~15.0.7",
"expo-status-bar": "^3.0.8",
"expo-web-browser": "^15.0.9",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.47.0",
"react-native": "0.81.5",
"react-native-health-connect": "^3.5.0",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-svg": "^15.15.0",
@@ -7236,6 +7239,56 @@
"expo": "*"
}
},
"node_modules/expo-dev-client": {
"version": "6.0.18",
"resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-6.0.18.tgz",
"integrity": "sha512-8QKWvhsoZpMkecAMlmWoRHnaTNiPS3aO7E42spZOMjyiaNRJMHZsnB8W2b63dt3Yg3oLyskLAoI8IOmnqVX8vA==",
"license": "MIT",
"dependencies": {
"expo-dev-launcher": "6.0.18",
"expo-dev-menu": "7.0.17",
"expo-dev-menu-interface": "2.0.0",
"expo-manifests": "~1.0.9",
"expo-updates-interface": "~2.0.0"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-dev-launcher": {
"version": "6.0.18",
"resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-6.0.18.tgz",
"integrity": "sha512-JTtcIfNvHO9PTdRJLmHs+7HJILXXZjF95jxgzu6hsJrgsTg/AZDtEsIt/qa6ctEYQTqrLdsLDgDhiXVel3AoQA==",
"license": "MIT",
"dependencies": {
"expo-dev-menu": "7.0.17",
"expo-manifests": "~1.0.9"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-dev-menu": {
"version": "7.0.17",
"resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-7.0.17.tgz",
"integrity": "sha512-NIu7TdaZf+A8+DROa6BB6lDfxjXxwaD+Q8QbNSVa0E0x6yl3P0ZJ80QbD2cCQeBzlx3Ufd3hNhczQWk4+A29HQ==",
"license": "MIT",
"dependencies": {
"expo-dev-menu-interface": "2.0.0"
},
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-dev-menu-interface": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-2.0.0.tgz",
"integrity": "sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-file-system": {
"version": "19.0.19",
"resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.19.tgz",
@@ -7269,6 +7322,12 @@
"expo": "*"
}
},
"node_modules/expo-json-utils": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz",
"integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==",
"license": "MIT"
},
"node_modules/expo-linear-gradient": {
"version": "15.0.7",
"resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-15.0.7.tgz",
@@ -7294,6 +7353,19 @@
"react-native": "*"
}
},
"node_modules/expo-manifests": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-1.0.9.tgz",
"integrity": "sha512-5uVgvIo0o+xBcEJiYn4uVh72QSIqyHePbYTWXYa4QamXd+AmGY/yWmtHaNqCqjsPLCwXyn4OxPr7jXJCeTWLow==",
"license": "MIT",
"dependencies": {
"@expo/config": "~12.0.10",
"expo-json-utils": "~0.15.0"
},
"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",
@@ -7435,6 +7507,19 @@
"expo": "*"
}
},
"node_modules/expo-sensors": {
"version": "15.0.7",
"resolved": "https://registry.npmjs.org/expo-sensors/-/expo-sensors-15.0.7.tgz",
"integrity": "sha512-TGUxRx/Ss7KGgfWo453YF64ENucw6oYryPiu/8I3ZZuf114xQPRxAbsZohPLaVUUGuaUyWbDsb0eRsmuKUzBnQ==",
"license": "MIT",
"dependencies": {
"invariant": "^2.2.4"
},
"peerDependencies": {
"expo": "*",
"react-native": "*"
}
},
"node_modules/expo-server": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.4.tgz",
@@ -7457,6 +7542,15 @@
"react-native": "*"
}
},
"node_modules/expo-updates-interface": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-2.0.0.tgz",
"integrity": "sha512-pTzAIufEZdVPKql6iMi5ylVSPqV1qbEopz9G6TSECQmnNde2nwq42PxdFBaUEd8IZJ/fdJLQnOT3m6+XJ5s7jg==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-web-browser": {
"version": "15.0.9",
"resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-15.0.9.tgz",
@@ -11613,6 +11707,27 @@
}
}
},
"node_modules/react-native-health-connect": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/react-native-health-connect/-/react-native-health-connect-3.5.0.tgz",
"integrity": "sha512-lIfiRps+jjDs83SApQ3AzBzU2cITwn8nPRl3v+52imWJPVw7qltAXkduioOfqFqOQJuEOwd1Wei3qtMTYbTaag==",
"license": "MIT",
"workspaces": [
"example",
"docs"
],
"engines": {
"node": ">= 16.0.0"
},
"funding": {
"url": "https://github.com/matinzd/react-native-health-connect?sponsor=1"
},
"peerDependencies": {
"@expo/config-plugins": ">= 6.0.2",
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-is-edge-to-edge": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz",
+5 -2
View File
@@ -5,8 +5,8 @@
"private": true,
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web",
"build": "expo build",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
@@ -28,6 +28,7 @@
"expo-camera": "~17.0.0",
"expo-constants": "^18.0.10",
"expo-crypto": "^15.0.7",
"expo-dev-client": "^6.0.18",
"expo-font": "~14.0.9",
"expo-haptics": "^15.0.7",
"expo-linear-gradient": "~15.0.7",
@@ -35,12 +36,14 @@
"expo-notifications": "~0.32.0",
"expo-router": "~6.0.14",
"expo-secure-store": "~15.0.7",
"expo-sensors": "~15.0.7",
"expo-status-bar": "^3.0.8",
"expo-web-browser": "^15.0.9",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.47.0",
"react-native": "0.81.5",
"react-native-health-connect": "^3.5.0",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-svg": "^15.15.0",
+56 -50
View File
@@ -1,22 +1,43 @@
import { View, Text, StyleSheet, ScrollView, RefreshControl, Image } from "react-native";
import { useUser } from "@clerk/clerk-expo";
import { useUser, useAuth } from "@clerk/clerk-expo";
import { LinearGradient } from "expo-linear-gradient";
import { useState, useCallback } from "react";
import { useState, useCallback, useEffect } from "react";
import { theme } from "../../styles/theme";
import { ActivityWidget } from "../../components/ActivityWidget";
import { QuickActionGrid } from "../../components/QuickActionGrid";
import { Ionicons } from "@expo/vector-icons";
import { apiClient } from "../../services/apiService";
import { useStepCount } from "../../hooks/useStepCount";
export default function HomeScreen() {
const { user } = useUser();
const { getToken } = useAuth();
const [refreshing, setRefreshing] = useState(false);
const [profile, setProfile] = useState<{ steps: number; calories: number; duration: number } | null>(null);
const [recentActivities, setRecentActivities] = useState<Array<any>>([]);
const { steps: deviceSteps, available } = useStepCount();
const loadData = useCallback(async () => {
try {
const token = await getToken();
const fitness = await apiClient.getFitnessProfile(token);
setProfile({ steps: fitness.steps, calories: fitness.calories, duration: fitness.duration });
const recent = await apiClient.getRecentActivities(token);
setRecentActivities(recent);
} catch (e) {
console.error(e);
}
}, [getToken]);
// Load data on component mount
useEffect(() => {
loadData();
}, []);
const onRefresh = useCallback(() => {
setRefreshing(true);
setTimeout(() => {
setRefreshing(false);
}, 2000);
}, []);
loadData().finally(() => setRefreshing(false));
}, [loadData]);
const getGreeting = () => {
const hour = new Date().getHours();
@@ -25,6 +46,16 @@ export default function HomeScreen() {
return "Good Evening";
};
// Use device steps if available, otherwise use profile steps
const displayedSteps = available ? deviceSteps : profile?.steps ?? 0;
console.log('HomeScreen Debug:', {
available,
deviceSteps,
profileSteps: profile?.steps,
displayedSteps
});
return (
<View style={styles.container}>
<ScrollView
@@ -51,11 +82,9 @@ export default function HomeScreen() {
</View>
{/* Activity Widget */}
<ActivityWidget
steps={8432}
calories={640}
duration={45}
/>
{profile && (
<ActivityWidget steps={displayedSteps} calories={profile.calories} duration={profile.duration} />
)}
{/* Quick Actions */}
<QuickActionGrid />
@@ -67,45 +96,22 @@ export default function HomeScreen() {
<Text style={styles.seeAll}>See All</Text>
</View>
<View style={styles.activityCard}>
<LinearGradient
colors={['rgba(255, 255, 255, 0.8)', 'rgba(255, 255, 255, 0.5)']}
style={[styles.recentItem, theme.shadows.subtle]}
>
<View style={styles.recentIconContainer}>
<LinearGradient
colors={theme.gradients.primary}
style={styles.recentIcon}
>
<Ionicons name="barbell" size={20} color="#fff" />
</LinearGradient>
</View>
<View style={styles.recentInfo}>
<Text style={styles.recentTitle}>Upper Body Power</Text>
<Text style={styles.recentSubtitle}>Today, 10:00 AM</Text>
</View>
<Text style={styles.recentValue}>45m</Text>
</LinearGradient>
<LinearGradient
colors={['rgba(255, 255, 255, 0.8)', 'rgba(255, 255, 255, 0.5)']}
style={[styles.recentItem, theme.shadows.subtle]}
>
<View style={styles.recentIconContainer}>
<LinearGradient
colors={theme.gradients.success}
style={styles.recentIcon}
>
<Ionicons name="bicycle" size={20} color="#fff" />
</LinearGradient>
</View>
<View style={styles.recentInfo}>
<Text style={styles.recentTitle}>Morning Cardio</Text>
<Text style={styles.recentSubtitle}>Yesterday, 7:30 AM</Text>
</View>
<Text style={styles.recentValue}>30m</Text>
</LinearGradient>
</View>
{recentActivities.map((act: any, idx: number) => (
<View key={idx} style={styles.recentItem}>
<LinearGradient colors={['rgba(255,255,255,0.8)', 'rgba(255,255,255,0.5)']} style={[styles.recentItem, theme.shadows.subtle]}>
<View style={styles.recentIconContainer}>
<LinearGradient colors={(theme.gradients as any)[act.gradient || 'primary']} style={styles.recentIcon}>
<Ionicons name={act.icon || 'barbell'} size={20} color="#fff" />
</LinearGradient>
</View>
<View style={styles.recentInfo}>
<Text style={styles.recentTitle}>{act.title}</Text>
<Text style={styles.recentSubtitle}>{act.subtitle}</Text>
</View>
<Text style={styles.recentValue}>{act.duration}m</Text>
</LinearGradient>
</View>
))}
</View>
{/* Bottom Spacer for Tab Bar */}
+1 -1
View File
@@ -1,5 +1,5 @@
export const API_BASE_URL = __DEV__
? 'https://cd1b5e914b16.ngrok-free.app'
? 'https://ef85a57e6911.ngrok-free.app'
: 'https://your-production-url.com'
export const API_ENDPOINTS = {
+152
View File
@@ -0,0 +1,152 @@
import { useEffect, useState, useRef } from "react";
import { Pedometer } from "expo-sensors";
import { Platform } from "react-native";
import { initialize, requestPermission, readRecords } from "react-native-health-connect";
import { apiClient } from "../services/apiService";
import { useAuth } from "@clerk/clerk-expo";
export const useStepCount = () => {
const [steps, setSteps] = useState(0);
const [available, setAvailable] = useState<boolean | null>(null);
const { getToken } = useAuth();
const stepsRef = useRef(0);
useEffect(() => {
let subscription: any = null;
let syncInterval: NodeJS.Timeout | null = null;
const initPedometer = async () => {
if (Platform.OS === 'android') {
try {
// Initialize Health Connect
const isInitialized = await initialize();
if (!isInitialized) {
console.log('Health Connect not initialized');
setAvailable(false);
return;
}
// Request permissions
const permissions = [
{ accessType: 'read', recordType: 'Steps' },
] as any[];
const granted = await requestPermission(permissions);
// Check if permissions granted (simplified check)
if (granted) {
setAvailable(true);
// Function to fetch steps
const fetchSteps = async () => {
const end = new Date();
const start = new Date();
start.setHours(0, 0, 0, 0);
const result = await readRecords('Steps', {
timeRangeFilter: {
operator: 'between',
startTime: start.toISOString(),
endTime: end.toISOString(),
},
});
const totalSteps = result.records.reduce((sum: number, record: any) => sum + record.count, 0);
console.log('Health Connect steps:', totalSteps);
setSteps(totalSteps);
stepsRef.current = totalSteps;
};
// Initial fetch
await fetchSteps();
// Poll for updates on Android (Health Connect doesn't have a real-time listener like Pedometer)
// We'll reuse the sync interval for this or create a separate one
syncInterval = setInterval(async () => {
await fetchSteps();
// Sync to backend
const currentSteps = stepsRef.current;
if (currentSteps > 0) {
try {
const token = await getToken();
await apiClient.syncSteps(currentSteps, token);
console.log('Synced steps:', currentSteps);
} catch (error) {
console.error('Failed to sync steps:', error);
}
}
}, 5000); // Check every 5 seconds for UI updates
} else {
console.log('Health Connect permissions denied');
setAvailable(false);
}
} catch (error) {
console.error('Error initializing Health Connect:', error);
setAvailable(false);
}
} else {
// iOS Implementation (Pedometer)
try {
const perm = await Pedometer.requestPermissionsAsync();
if (perm.status === 'denied') {
setAvailable(false);
return;
}
const isAvailable = await Pedometer.isAvailableAsync();
setAvailable(isAvailable);
if (isAvailable) {
const end = new Date();
const start = new Date();
start.setHours(0, 0, 0, 0);
const pastStepCount = await Pedometer.getStepCountAsync(start, end);
if (pastStepCount) {
setSteps(pastStepCount.steps);
stepsRef.current = pastStepCount.steps;
}
subscription = Pedometer.watchStepCount(result => {
const newSteps = stepsRef.current + result.steps;
setSteps(newSteps);
});
syncInterval = setInterval(async () => {
const currentSteps = stepsRef.current;
if (currentSteps > 0) {
try {
const token = await getToken();
await apiClient.syncSteps(currentSteps, token);
} catch (error) {
console.error('Failed to sync steps:', error);
}
}
}, 30000);
}
} catch (error) {
console.error('Error initializing Pedometer:', error);
}
}
};
initPedometer();
return () => {
if (subscription) {
subscription.remove();
}
if (syncInterval) {
clearInterval(syncInterval);
}
};
}, [getToken]);
// Update ref when steps change
useEffect(() => {
stepsRef.current = steps;
}, [steps]);
return { steps, available };
};
+74
View File
@@ -0,0 +1,74 @@
import { API_BASE_URL, API_ENDPOINTS } from '../config/api';
export const apiClient = {
async getFitnessProfile(token?: string | null) {
try {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(token && { 'Authorization': `Bearer ${token}` }),
};
const res = await fetch(`${API_BASE_URL}${API_ENDPOINTS.PROFILE.FITNESS}`, { headers });
if (!res.ok) {
throw new Error('Failed to fetch fitness profile');
}
return await res.json();
} catch (e) {
console.warn('Using mock fitness profile due to fetch error', e);
// Mock data shape should match backend response
return { steps: 0, calories: 0, duration: 0 };
}
},
async getRecentActivities(token?: string | null) {
// Placeholder endpoint – adjust to match backend implementation
try {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(token && { 'Authorization': `Bearer ${token}` }),
};
const res = await fetch(`${API_BASE_URL}/api/fitness-goals/recent`, { headers });
if (!res.ok) {
throw new Error('Failed to fetch recent activities');
}
return await res.json();
} catch (e) {
console.warn('Using mock recent activities due to fetch error', e);
// Return empty array or sample data
return [];
}
},
async syncSteps(steps: number, token?: string | null) {
try {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(token && { 'Authorization': `Bearer ${token}` }),
};
const res = await fetch(`${API_BASE_URL}/api/activity/steps`, {
method: 'POST',
headers,
body: JSON.stringify({ steps, timestamp: new Date().toISOString() }),
});
if (!res.ok) throw new Error('Failed to sync steps');
return await res.json();
} catch (e) {
console.warn('Failed to sync steps', e);
throw e;
}
},
async getDailyActivity(date?: string, token?: string | null) {
try {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(token && { 'Authorization': `Bearer ${token}` }),
};
const dateParam = date || new Date().toISOString().split('T')[0];
const res = await fetch(`${API_BASE_URL}/api/activity/daily?date=${dateParam}`, { headers });
if (!res.ok) throw new Error('Failed to fetch daily activity');
return await res.json();
} catch (e) {
console.warn('Failed to fetch daily activity', e);
return { steps: 0, calories: 0, duration: 0 };
}
},
};