Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Location permissions for GPS tracking -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
Comment on lines +5 to +6
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

ACCESS_BACKGROUND_LOCATION permission requires special justification on Android 10+ and should only be requested if the app truly needs to track location in the background. If background tracking is not implemented, this permission should be removed. Also consider checking if FOREGROUND_SERVICE permission is actually used by implementing a foreground service for tracking.

Suggested change
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

Copilot uses AI. Check for mistakes.
<uses-permission android:name="android.permission.INTERNET" />

<application
android:label="vector"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">

<activity
android:name=".MainActivity"
android:exported="true"
Expand Down
8 changes: 8 additions & 0 deletions lib/core/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ const int colorAccent = 0xFFEE94FE;
const int colorTextPrimary = 0xFFF5F5F5;
const int colorTextSecondary = 0xFFFFFFFF;

// Session Page Colors
const int colorSessionDarkPurple = 0xFF724C94;
const int colorSessionPink = 0xFFAA4FA9;
const int colorSessionCard = 0xFFC675AF;

// Database Paths - Sessions
const String sessionsPath = 'sessions';

// Sizes
const double paddingSmall = 8.0;
const double paddingMedium = 16.0;
Expand Down
Empty file.
63 changes: 63 additions & 0 deletions lib/core/services/firebase_auth_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'firebase_service.dart';

/// Firebase Authentication Service
/// Uses the central FirebaseService instance for all auth operations.
class FirebaseAuthService {
final FirebaseAuth _auth = FirebaseService.instance.auth;

User? get currentUser => _auth.currentUser;

bool get isAuthenticated => currentUser != null;

String? get userId => currentUser?.uid;

Stream<User?> get authStateChanges => _auth.authStateChanges();

Future<UserCredential> signUp({
required String email,
required String password,
}) async {
return await _auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
}

Future<UserCredential> signIn({
required String email,
required String password,
}) async {
return await _auth.signInWithEmailAndPassword(
email: email,
password: password,
);
}

Future<void> sendPasswordResetEmail({required String email}) async {
await _auth.sendPasswordResetEmail(email: email);
}

Future<void> confirmPasswordReset({
required String code,
required String newPassword,
}) async {
await _auth.confirmPasswordReset(code: code, newPassword: newPassword);
}

Future<void> updatePassword({required String newPassword}) async {
await currentUser?.updatePassword(newPassword);
}

Future<void> sendEmailVerification() async {
await currentUser?.sendEmailVerification();
}

Future<void> signOut() async {
await _auth.signOut();
}

Future<void> deleteAccount() async {
await currentUser?.delete();
}
}
6 changes: 4 additions & 2 deletions lib/core/services/firebase_database_service.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import 'package:firebase_database/firebase_database.dart';
import '../../models/onboarding_data_model.dart';
import '../constants.dart';
import 'firebase_service.dart';

/// Firebase Realtime Database Service
/// Uses the central FirebaseService instance for all database operations.
class FirebaseDatabaseService {
final FirebaseDatabase _database = FirebaseDatabase.instance;
final FirebaseDatabase _database = FirebaseService.instance.database;

DatabaseReference get _usersRef => _database.ref(usersPath);
DatabaseReference get _onboardingRef => _database.ref(onboardingPath);

Future<void> saveOnboardingData(
Expand Down
54 changes: 54 additions & 0 deletions lib/core/services/firebase_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_database/firebase_database.dart';

/// Central Firebase service that provides singleton instances
/// of all Firebase services used in the app.
///
/// This ensures we use pre-initialized instances and avoid
/// creating multiple connections to Firebase.
class FirebaseService {
// Private constructor for singleton pattern
FirebaseService._();

// Singleton instance
static final FirebaseService _instance = FirebaseService._();
static FirebaseService get instance => _instance;

// Firebase instances (lazy initialized after Firebase.initializeApp())
FirebaseAuth? _auth;
FirebaseDatabase? _database;

/// Initialize Firebase and cache the instances
static Future<void> initialize() async {
await Firebase.initializeApp();
_instance._auth = FirebaseAuth.instance;
_instance._database = FirebaseDatabase.instance;
}

/// Get the FirebaseAuth instance
FirebaseAuth get auth {
if (_auth == null) {
throw Exception(
'FirebaseService not initialized. Call FirebaseService.initialize() first.',
);
}
return _auth!;
}

/// Get the FirebaseDatabase instance
FirebaseDatabase get database {
if (_database == null) {
throw Exception(
'FirebaseService not initialized. Call FirebaseService.initialize() first.',
);
}
return _database!;
}

// Convenience getters
User? get currentUser => auth.currentUser;
bool get isAuthenticated => currentUser != null;
String? get userId => currentUser?.uid;
Stream<User?> get authStateChanges => auth.authStateChanges();
}
178 changes: 178 additions & 0 deletions lib/core/services/session_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import 'package:firebase_database/firebase_database.dart';
import 'package:vector/models/session_tracking_model.dart';
import 'package:vector/core/constants.dart';
import 'package:vector/core/services/firebase_service.dart';

/// Service for managing session data in Firebase Realtime Database
class SessionService {
final FirebaseDatabase _database = FirebaseService.instance.database;

DatabaseReference get _sessionsRef => _database.ref(sessionsPath);

/// Start a new session
Future<String> startSession({
required String userId,
required ActivityType activityType,
}) async {
final session = SessionModel(
userId: userId,
activityType: activityType,
startTime: DateTime.now(),
isActive: true,
);

final newRef = _sessionsRef.child(userId).push();
await newRef.set(session.toJson());
return newRef.key!;
}

/// Update session with new location data
Future<void> updateSession({
required String userId,
required String sessionId,
required SessionModel session,
}) async {
await _sessionsRef.child(userId).child(sessionId).update(session.toJson());
}

/// End a session
Future<void> endSession({
required String userId,
required String sessionId,
required SessionModel session,
}) async {
final endedSession = session.copyWith(
endTime: DateTime.now(),
isActive: false,
);
await _sessionsRef
.child(userId)
.child(sessionId)
.set(endedSession.toJson());
}

/// Get all sessions for a user
Future<List<SessionModel>> getUserSessions(String userId) async {
final snapshot = await _sessionsRef.child(userId).get();

if (!snapshot.exists || snapshot.value == null) {
return [];
}

final data = Map<String, dynamic>.from(snapshot.value as Map);
return data.entries.map((e) {
return SessionModel.fromJson(
Map<String, dynamic>.from(e.value),
id: e.key,
);
}).toList();
}

/// Get today's sessions for a user
Future<List<SessionModel>> getTodaySessions(String userId) async {
final allSessions = await getUserSessions(userId);
final today = DateTime.now();
final startOfDay = DateTime(today.year, today.month, today.day);

return allSessions.where((s) => s.startTime.isAfter(startOfDay)).toList();
}

/// Get sessions for a specific date range
Future<List<SessionModel>> getSessionsInRange(
String userId, {
required DateTime startDate,
required DateTime endDate,
}) async {
final allSessions = await getUserSessions(userId);
return allSessions
.where(
(s) =>
s.startTime.isAfter(startDate) &&
s.startTime.isBefore(endDate.add(const Duration(days: 1))),
)
.toList()
..sort((a, b) => b.startTime.compareTo(a.startTime)); // Most recent first
}

/// Get sessions for the past week
Future<List<SessionModel>> getWeekSessions(String userId) async {
final now = DateTime.now();
final weekAgo = now.subtract(const Duration(days: 7));
return getSessionsInRange(userId, startDate: weekAgo, endDate: now);
}

/// Get sessions for the past month
Future<List<SessionModel>> getMonthSessions(String userId) async {
final now = DateTime.now();
final monthAgo = DateTime(now.year, now.month - 1, now.day);
return getSessionsInRange(userId, startDate: monthAgo, endDate: now);
}

/// Get sessions for the past year
Future<List<SessionModel>> getYearSessions(String userId) async {
final now = DateTime.now();
final yearAgo = DateTime(now.year - 1, now.month, now.day);
return getSessionsInRange(userId, startDate: yearAgo, endDate: now);
}

/// Get the active session if any
Future<SessionModel?> getActiveSession(String userId) async {
final sessions = await getUserSessions(userId);
try {
return sessions.firstWhere((s) => s.isActive);
} catch (_) {
return null;
}
}

/// Delete a session
Future<void> deleteSession({
required String userId,
required String sessionId,
}) async {
await _sessionsRef.child(userId).child(sessionId).remove();
}

/// Calculate calories burned based on activity type and distance
static double calculateCalories({
required ActivityType activityType,
required double distanceMeters,
required double weightKg,
}) {
// MET values (Metabolic Equivalent of Task)
// Walking: 3.5 MET, Running: 9.8 MET, Cycling: 7.5 MET
double met;
switch (activityType) {
case ActivityType.walking:
met = 3.5;
break;
case ActivityType.running:
met = 9.8;
break;
case ActivityType.cycling:
met = 7.5;
break;
}

// Estimate duration based on average speeds
// Walking: 5 km/h, Running: 10 km/h, Cycling: 20 km/h
double avgSpeedKmh;
switch (activityType) {
case ActivityType.walking:
avgSpeedKmh = 5.0;
break;
case ActivityType.running:
avgSpeedKmh = 10.0;
break;
case ActivityType.cycling:
avgSpeedKmh = 20.0;
break;
}

final distanceKm = distanceMeters / 1000;
final durationHours = distanceKm / avgSpeedKmh;

// Calories = MET × weight (kg) × time (hours)
return met * weightKg * durationHours;
Comment on lines +157 to +176
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The calculation assumes fixed average speeds for each activity type, which may lead to inaccurate calorie estimates. Since the actual session duration is available in the SessionModel, use the real duration instead of estimating it from distance and assumed speed.

Copilot uses AI. Check for mistakes.
}
}
20 changes: 13 additions & 7 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:firebase_core/firebase_core.dart';

import 'package:vector/view_models/home_view_model.dart';
import 'package:vector/view_models/onboarding_view_model.dart';
import 'package:vector/core/utils/firebase_test_util.dart';
import 'package:vector/views/welcome.dart';
import 'view_models/home_view_model.dart';
import 'view_models/onboarding_view_model.dart';
import 'view_models/auth_view_model.dart';
import 'view_models/session_view_model.dart';
import 'view_models/profile_view_model.dart';
import 'core/services/firebase_service.dart';
import 'core/utils/firebase_test_util.dart';
import 'widgets/auth_wrapper.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
await FirebaseService.initialize();

// Run Firebase tests in debug mode
assert(() {
Expand All @@ -27,8 +30,11 @@ class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthViewModel()),
ChangeNotifierProvider(create: (_) => HomeViewModel()),
ChangeNotifierProvider(create: (_) => OnboardingViewModel()),
ChangeNotifierProvider(create: (_) => SessionViewModel()),
ChangeNotifierProvider(create: (_) => ProfileViewModel()),
],
child: MaterialApp(
title: 'Vector',
Expand All @@ -37,7 +43,7 @@ class MyApp extends StatelessWidget {
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const WelcomePage(),
home: const AuthWrapper(),
),
);
}
Expand Down
Loading