Skip to content
Merged
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
2 changes: 2 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<uses-feature android:name="android.hardware.camera" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:label="ecoscan"
android:name="${applicationName}"
Expand Down
3 changes: 2 additions & 1 deletion ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import Flutter
import UIKit

import GoogleMaps
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GMSServices.provideAPIKey("YOUR_iOS_API_KEY_HERE")

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@Shaam143 API Key Google maps (IOS)

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

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

Hardcoding an API key placeholder in source code is risky and will also break maps unless manually edited per developer. Load the Google Maps API key from build configuration (e.g., Info.plist / xcconfig) and keep it out of version control; avoid committing placeholders in the app delegate.

Suggested change
GMSServices.provideAPIKey("YOUR_iOS_API_KEY_HERE")
if let apiKey = Bundle.main.object(forInfoDictionaryKey: "GOOGLE_MAPS_API_KEY") as? String,
!apiKey.isEmpty {
GMSServices.provideAPIKey(apiKey)
}

Copilot uses AI. Check for mistakes.
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

Expand Down
2 changes: 1 addition & 1 deletion lib/core/widgets/impact_row.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class ImpactRow extends StatelessWidget {
LinearProgressIndicator(
value: value,
color: color,
backgroundColor: color.withOpacity(0.1),
backgroundColor: color.withValues(alpha: 0.1),
minHeight: 12,
borderRadius: BorderRadius.circular(10),
),
Expand Down
2 changes: 1 addition & 1 deletion lib/core/widgets/smart_result_modal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class SmartResultModal extends StatelessWidget {
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
decoration: BoxDecoration(
color: themeColor.withOpacity(0.1),
color: themeColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: themeColor, width: 2),
),
Expand Down
15 changes: 15 additions & 0 deletions lib/features/auth/data/auth_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,19 @@ class AuthRepository {
Future<void> logout() async {
await auth.signOut();
}
Future<void> changePassword({required String currentPassword, required String newPassword}) async {
final user = FirebaseAuth.instance.currentUser;
if (user == null || user.email == null) throw "User not found";

// 1. Re-authenticate the user (Required by Firebase for security)
AuthCredential credential = EmailAuthProvider.credential(
email: user.email!,
password: currentPassword,
);

await user.reauthenticateWithCredential(credential);

// 2. Update to new password
await user.updatePassword(newPassword);
}
}
Comment on lines +25 to 40

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

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

This password-change method bypasses the injected auth instance by using FirebaseAuth.instance, which makes the class harder to test and inconsistent with the rest of the repository. It also throws a raw string and has inconsistent formatting. Prefer using the injected auth, throwing an Exception type, and aligning formatting with the rest of the file.

Suggested change
Future<void> changePassword({required String currentPassword, required String newPassword}) async {
final user = FirebaseAuth.instance.currentUser;
if (user == null || user.email == null) throw "User not found";
// 1. Re-authenticate the user (Required by Firebase for security)
AuthCredential credential = EmailAuthProvider.credential(
email: user.email!,
password: currentPassword,
);
await user.reauthenticateWithCredential(credential);
// 2. Update to new password
await user.updatePassword(newPassword);
}
}
Future<void> changePassword({
required String currentPassword,
required String newPassword,
}) async {
final user = auth.currentUser;
if (user == null || user.email == null) {
throw Exception('User not found');
}
// 1. Re-authenticate the user (Required by Firebase for security)
final AuthCredential credential = EmailAuthProvider.credential(
email: user.email!,
password: currentPassword,
);
await user.reauthenticateWithCredential(credential);
// 2. Update to new password
await user.updatePassword(newPassword);
}
}

Copilot uses AI. Check for mistakes.
8 changes: 4 additions & 4 deletions lib/features/auth/ui/register_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 20, offset: const Offset(0, 10)),
BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 10)),
],
),
child: Padding(
Expand Down Expand Up @@ -213,8 +213,8 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
),
child: Stack(
children: [
Positioned(top: -50, right: -50, child: _circleDeco(150, Colors.white.withOpacity(0.1))),
Positioned(top: 50, left: -20, child: _circleDeco(100, Colors.white.withOpacity(0.05))),
Positioned(top: -50, right: -50, child: _circleDeco(150, Colors.white.withValues(alpha: 0.1))),
Positioned(top: 50, left: -20, child: _circleDeco(100, Colors.white.withValues(alpha: 0.05))),
const Positioned(
top: 80,
left: 30,
Expand All @@ -240,7 +240,7 @@ class _RegisterScreenState extends ConsumerState<RegisterScreen> {
labelText: label,
prefixIcon: Icon(icon, color: Colors.green[700]),
filled: true,
fillColor: Colors.green.withOpacity(0.05),
fillColor: Colors.green.withValues(alpha: 0.05),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none),
enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none),
focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15), borderSide: const BorderSide(color: Colors.green, width: 2)),
Expand Down
14 changes: 7 additions & 7 deletions lib/features/auth/ui/welcome_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class WelcomeScreen extends StatelessWidget {
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.green.withOpacity(0.05),
color: Colors.green.withValues(alpha: 0.05),
blurRadius: 20,
offset: const Offset(0, -5),
),
Expand Down Expand Up @@ -136,8 +136,8 @@ class WelcomeScreen extends StatelessWidget {
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned(top: -50, right: -20, child: _circleDeco(150, Colors.white.withOpacity(0.08))),
Positioned(bottom: -30, left: -40, child: _circleDeco(120, Colors.white.withOpacity(0.05))),
Positioned(top: -50, right: -20, child: _circleDeco(150, Colors.white.withValues(alpha: 0.08))),
Positioned(bottom: -30, left: -40, child: _circleDeco(120, Colors.white.withValues(alpha: 0.05))),

Center(
child: Column(
Expand All @@ -149,7 +149,7 @@ class WelcomeScreen extends StatelessWidget {
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 20, offset: const Offset(0, 10)),
BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 10)),
],
),
child: const Icon(Icons.eco_rounded, size: 50, color: Color(0xFF1B5E20)),
Expand All @@ -163,7 +163,7 @@ class WelcomeScreen extends StatelessWidget {
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(20),
),
child: const Text(
Expand All @@ -186,14 +186,14 @@ class WelcomeScreen extends StatelessWidget {
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 15, offset: const Offset(0, 8)),
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 15, offset: const Offset(0, 8)),
],
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: iconColor.withOpacity(0.1), shape: BoxShape.circle),
decoration: BoxDecoration(color: iconColor.withValues(alpha: 0.1), shape: BoxShape.circle),
child: Icon(icon, color: iconColor, size: 28),
),
const SizedBox(width: 16),
Expand Down
9 changes: 4 additions & 5 deletions lib/features/home/controllers/analytics_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../models/user_model.dart';

// Use a simple enum for the filters
enum AnalyticsFilter { week, month, allTime }
Expand Down Expand Up @@ -30,9 +29,8 @@ class AnalyticsController extends AsyncNotifier<Map<String, double>> {
// INSTANT: Fetch from Category Counter in User Document
final doc = await FirebaseFirestore.instance.collection('users').doc(user.uid).get();
final data = doc.data()?['categoryCounts'] as Map<String, dynamic>? ?? {};

// Convert Map<String, int> to Map<String, double> for the UI bars
return data.map((key, value) => MapEntry(key, (value as num).toDouble()));
final Map<String, dynamic> rawData = Map<String, dynamic>.from(data);
return Map<String, double>.from(rawData.map((key, value) => MapEntry(key, (value as num).toDouble())));
Comment on lines +32 to +33

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

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

Unnecessary type conversion: The code converts data to Map<String, dynamic> and then converts back. The intermediate rawData variable adds complexity without clear benefit. Consider simplifying to: return Map<String, double>.from(data.map((key, value) => MapEntry(key, (value as num).toDouble())));

Suggested change
final Map<String, dynamic> rawData = Map<String, dynamic>.from(data);
return Map<String, double>.from(rawData.map((key, value) => MapEntry(key, (value as num).toDouble())));
return Map<String, double>.from(
data.map((key, value) => MapEntry(key, (value as num).toDouble())),
);

Copilot uses AI. Check for mistakes.
} else {
// DYNAMIC: Query Scans sub-collection for specific range
DateTime now = DateTime.now();
Expand All @@ -49,7 +47,8 @@ class AnalyticsController extends AsyncNotifier<Map<String, double>> {

Map<String, double> counts = {};
for (var doc in query.docs) {
final cat = doc.data()['category'] ?? 'General';
final data = doc.data();
final String cat = data['category']?.toString() ?? 'General';
counts[cat] = (counts[cat] ?? 0.0) + 1.0;
}
return counts;
Expand Down
42 changes: 42 additions & 0 deletions lib/features/home/controllers/centers_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import 'package:flutter_map/flutter_map.dart'; // 🌟 CHANGED: Replaced Google Maps
import 'package:latlong2/latlong.dart'; // 🌟 CHANGED: Using free LatLng
import 'package:geolocator/geolocator.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/recycleingcenter_model.dart';
import '../repositories/centers_repository.dart'; // Make sure this path matches your repo

final centersProvider = StreamNotifierProvider<CentersController, List<RecyclingCenterModel>>(() {
return CentersController();
});

class CentersController extends StreamNotifier<List<RecyclingCenterModel>> {
// 🌟 CHANGED: Initialized the OpenStreetMap controller
final MapController mapController = MapController();
String selectedCategory = "All";

@override
Stream<List<RecyclingCenterModel>> build() {
return ref.read(centersRepositoryProvider).getCentersStream();
}

void setFilter(String category) {
selectedCategory = category;
ref.invalidateSelf(); // Refresh the stream and UI
}
Comment on lines +22 to +25

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

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

Calling ref.invalidateSelf() in setFilter restarts the Firestore stream subscription every time the user changes the chip filter, which is unnecessary work and can increase reads/flicker. Prefer keeping the centers stream stable and storing selectedCategory in separate state (e.g., a StateProvider) or in the provider state, then filter the already-streamed list.

Copilot uses AI. Check for mistakes.

// 🌟 CHANGED: Using flutter_map's `.move()` syntax instead of animateCamera
void animateToCenter(double lat, double lng) {
mapController.move(LatLng(lat, lng), 15.0);
}

Future<Position> getCurrentLocation() async {
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) return Future.error('Location services are disabled.');

LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
Comment on lines +34 to +39

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

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

getCurrentLocation() requests permission only when denied but doesn't handle deniedForever or the case where the user still denies after requesting. As written, it will call getCurrentPosition() even without permission, which can throw. Add explicit handling for deniedForever and re-check permission after the request.

Suggested change
if (!serviceEnabled) return Future.error('Location services are disabled.');
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (!serviceEnabled) {
return Future.error('Location services are disabled.');
}
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.denied) {
return Future.error('Location permissions are denied');
}
if (permission == LocationPermission.deniedForever) {
return Future.error(
'Location permissions are permanently denied, we cannot request permissions.',
);
}

Copilot uses AI. Check for mistakes.
return await Geolocator.getCurrentPosition();
}
}
25 changes: 25 additions & 0 deletions lib/features/home/controllers/history_controller.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/scan_model.dart';
import '../repositories/history_repository.dart';

final scanHistoryProvider = StreamNotifierProvider<HistoryController, List<ScanModel>>(() {
return HistoryController();
});

class HistoryController extends StreamNotifier<List<ScanModel>> {
@override
Stream<List<ScanModel>> build() {
return ref.read(historyRepositoryProvider).getScanHistoryStream();
}

// Logic for the Floating Card metric
int getTodayScanCount(List<ScanModel> scans) {
final now = DateTime.now();
return scans.where((scan) {
final date = scan.timestamp.toDate();
return date.year == now.year &&
date.month == now.month &&
date.day == now.day;
}).length;
}
}
31 changes: 29 additions & 2 deletions lib/features/home/controllers/user_controller.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,36 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../models/user_model.dart';
import '../repositories/user_repository.dart';

final userControllerProvider =
StreamProvider.family<UserModel, String>((ref, uid) {
// 🟢 DATA PROVIDER: Keep this as StreamProvider so your UI doesn't break
final userControllerProvider = StreamProvider.family<UserModel, String>((ref, uid) {
final repo = ref.watch(userRepositoryProvider);
return repo.getUser(uid);
});

// 🔵 ACTION PROVIDER: Use this for updating profile settings
final userProfileActionsProvider = Provider((ref) => UserProfileActions(ref));

class UserProfileActions {
final Ref ref;
UserProfileActions(this.ref);

Future<void> updateProfile({
required String username,
required String email,
String? profileUrl,
}) async {
final uid = FirebaseAuth.instance.currentUser?.uid;
if (uid == null) return;

final updates = {
'username': username,
'email': email,
// Use the null-aware spread as suggested by your diagnostic
...?profileUrl != null ? {'profileurl': profileUrl} : null,
};

Comment on lines +30 to +33

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

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

This map spread is invalid Dart and will not compile (...?profileUrl != null ? ...). Use a conditional map entry/spread that Dart supports (e.g., add the key only when profileUrl != null, or spread an empty map otherwise).

Suggested change
// Use the null-aware spread as suggested by your diagnostic
...?profileUrl != null ? {'profileurl': profileUrl} : null,
};
if (profileUrl != null) 'profileurl': profileUrl,
};

Copilot uses AI. Check for mistakes.
await ref.read(userRepositoryProvider).updateUserSettings(uid, updates);
}
}
6 changes: 5 additions & 1 deletion lib/features/home/models/user_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class UserModel {
final int streak;
final Map<String, int> categoryCounts;
final double nextMilestoneCo2;
final String lastScanWeekId;

UserModel({
required this.id,
Expand All @@ -25,6 +26,7 @@ class UserModel {
required this.streak,
required this.categoryCounts,
required this.nextMilestoneCo2,
required this.lastScanWeekId,
});

factory UserModel.fromMap(String id, Map<String, dynamic> json) {
Expand All @@ -40,7 +42,8 @@ class UserModel {
rankTier: json['rankTier'] ?? 'Bronze',
streak: json['streak'] ?? 0,
categoryCounts: Map<String, int>.from(json['categoryCounts'] ?? {}),
nextMilestoneCo2: (json['nextMilestoneCo2'] ?? 20.0).toDouble(),
nextMilestoneCo2: (json['nextMilestoneCo2'] ?? 50.0).toDouble(),
lastScanWeekId: json['lastScanWeekId'] ?? '',
);
}

Expand All @@ -57,6 +60,7 @@ class UserModel {
'streak': streak,
'categoryCounts': categoryCounts,
'nextMilestoneCo2': nextMilestoneCo2,
'lastScanWeekId': lastScanWeekId,
};
}
}
18 changes: 18 additions & 0 deletions lib/features/home/repositories/centers_repository.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/recycleingcenter_model.dart';

final centersRepositoryProvider = Provider((ref) => CentersRepository());

class CentersRepository {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;

Stream<List<RecyclingCenterModel>> getCentersStream() {
return _firestore
.collection('recyclingCenters')
.snapshots()
.map((snapshot) => snapshot.docs
.map((doc) => RecyclingCenterModel.fromMap(doc.data()))
.toList());
}
}
27 changes: 27 additions & 0 deletions lib/features/home/repositories/history_repository.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/scan_model.dart';

final historyRepositoryProvider = Provider((ref) => HistoryRepository());

class HistoryRepository {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final FirebaseAuth _auth = FirebaseAuth.instance;

Stream<List<ScanModel>> getScanHistoryStream() {
final uid = _auth.currentUser?.uid;
if (uid == null) return Stream.value([]);

// Streams the specific user's scan sub-collection in real-time
return _firestore
.collection('users')
.doc(uid)
.collection('scans')
.orderBy('timestamp', descending: true) // Newest first
.snapshots()
.map((snapshot) {
return snapshot.docs.map((doc) => ScanModel.fromMap(doc.data())).toList();
});
}
}
Loading