Skip to content

Commit 02af25b

Browse files
author
BiasGuard Member B
committed
Google Solution Challenge 2026: End-to-End Full-Stack Integration & Ultimate Polish
1 parent 5671bf3 commit 02af25b

23 files changed

Lines changed: 2168 additions & 818 deletions

File tree

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ firebase-debug.*.log
4141
.DS_Store
4242
Thumbs.db
4343

44+
# Internal Development & Testing
45+
testsprite_tests/
46+
biasguard_app/testsprite_tests/
47+
testsprite-mcp-test-report.md
48+
tmp/
49+
*.log
50+
4451
# Keys & secrets
4552
*.key
4653
*.pem
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import 'package:flutter_riverpod/flutter_riverpod.dart';
2+
import 'package:shared_preferences/shared_preferences.dart';
3+
4+
enum AppLocale { en, hi }
5+
6+
class LocaleNotifier extends StateNotifier<AppLocale> {
7+
LocaleNotifier() : super(AppLocale.en) {
8+
_loadLocale();
9+
}
10+
11+
static const _key = 'app_locale';
12+
13+
Future<void> _loadLocale() async {
14+
final prefs = await SharedPreferences.getInstance();
15+
final val = prefs.getString(_key);
16+
if (val == 'hi') {
17+
state = AppLocale.hi;
18+
} else {
19+
state = AppLocale.en;
20+
}
21+
}
22+
23+
Future<void> toggleLocale() async {
24+
final prefs = await SharedPreferences.getInstance();
25+
if (state == AppLocale.en) {
26+
state = AppLocale.hi;
27+
await prefs.setString(_key, 'hi');
28+
} else {
29+
state = AppLocale.en;
30+
await prefs.setString(_key, 'en');
31+
}
32+
}
33+
34+
bool get isHindi => state == AppLocale.hi;
35+
}
36+
37+
final localeProvider = StateNotifierProvider<LocaleNotifier, AppLocale>((ref) {
38+
return LocaleNotifier();
39+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_riverpod/flutter_riverpod.dart';
3+
import 'package:shared_preferences/shared_preferences.dart';
4+
5+
final themeProvider = StateNotifierProvider<ThemeNotifier, ThemeMode>((ref) {
6+
return ThemeNotifier();
7+
});
8+
9+
class ThemeNotifier extends StateNotifier<ThemeMode> {
10+
ThemeNotifier() : super(ThemeMode.dark) {
11+
_loadTheme();
12+
}
13+
14+
static const _themeKey = 'theme_mode';
15+
16+
Future<void> _loadTheme() async {
17+
final prefs = await SharedPreferences.getInstance();
18+
final isDark = prefs.getBool(_themeKey) ?? true;
19+
state = isDark ? ThemeMode.dark : ThemeMode.light;
20+
}
21+
22+
Future<void> toggleTheme() async {
23+
state = state == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark;
24+
final prefs = await SharedPreferences.getInstance();
25+
await prefs.setBool(_themeKey, state == ThemeMode.dark);
26+
}
27+
28+
bool get isDarkMode => state == ThemeMode.dark;
29+
}

biasguard_app/lib/core/router/app_router.dart

Lines changed: 80 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import 'package:flutter/material.dart';
44
import 'package:go_router/go_router.dart';
55
import 'package:flutter_riverpod/flutter_riverpod.dart';
6+
import 'package:shared_preferences/shared_preferences.dart';
67

78
import '../../features/auth/screens/login_screen.dart';
89
import '../../features/dashboard/screens/dashboard_screen.dart';
@@ -16,6 +17,9 @@ import '../../features/report/screens/report_screen.dart';
1617
import '../../features/settings/screens/settings_screen.dart';
1718
import '../../features/profile/screens/profile_screen.dart';
1819
import '../../features/about/screens/about_screen.dart';
20+
import '../features/onboarding/screens/onboarding_screen.dart';
21+
import '../features/settings/screens/privacy_policy_screen.dart';
22+
import '../features/settings/screens/terms_screen.dart';
1923

2024
class AppRoutes {
2125
static const login = '/login';
@@ -30,18 +34,53 @@ class AppRoutes {
3034
static const settings = '/settings';
3135
static const profile = '/profile';
3236
static const about = '/about';
37+
static const onboarding = '/onboarding';
38+
static const privacy = '/settings/privacy';
39+
static const terms = '/settings/terms';
3340
}
3441

42+
final onboardingProvider = FutureProvider<bool>((ref) async {
43+
final prefs = await SharedPreferences.getInstance();
44+
return prefs.getBool('onboarding_complete') ?? false;
45+
});
46+
3547
final appRouterProvider = Provider<GoRouter>((ref) {
48+
final onboardingComplete = ref.watch(onboardingProvider);
49+
3650
return GoRouter(
37-
initialLocation: AppRoutes.login,
51+
initialLocation: AppRoutes.dashboard,
52+
redirect: (context, state) {
53+
if (onboardingComplete.isLoading) return null;
54+
55+
final completed = onboardingComplete.value ?? false;
56+
final isGoingToOnboarding = state.matchedLocation == AppRoutes.onboarding;
57+
58+
if (!completed && !isGoingToOnboarding) {
59+
return AppRoutes.onboarding;
60+
}
61+
62+
// If completed and trying to go back to onboarding, redirect to home
63+
if (completed && isGoingToOnboarding) {
64+
return AppRoutes.dashboard;
65+
}
66+
67+
if (state.uri.toString() == '/' || state.uri.toString() == '') {
68+
return AppRoutes.dashboard;
69+
}
70+
return null;
71+
},
3872
debugLogDiagnostics: true,
3973
routes: [
4074
GoRoute(
4175
path: AppRoutes.login,
4276
name: 'login',
4377
builder: (context, state) => const LoginScreen(),
4478
),
79+
GoRoute(
80+
path: AppRoutes.onboarding,
81+
name: 'onboarding',
82+
builder: (context, state) => const OnboardingScreen(),
83+
),
4584
ShellRoute(
4685
builder: (context, state, child) => AppShell(child: child),
4786
routes: [
@@ -105,8 +144,17 @@ final appRouterProvider = Provider<GoRouter>((ref) {
105144
name: 'settings',
106145
builder: (context, state) => const SettingsScreen(),
107146
),
147+
GoRoute(
148+
path: AppRoutes.privacy,
149+
builder: (context, state) => const PrivacyPolicyScreen(),
150+
),
151+
GoRoute(
152+
path: AppRoutes.terms,
153+
builder: (context, state) => const TermsScreen(),
154+
),
108155
GoRoute(
109156
path: AppRoutes.profile,
157+
110158
name: 'profile',
111159
builder: (context, state) => const ProfileScreen(),
112160
),
@@ -211,32 +259,37 @@ class _NavItem extends StatelessWidget {
211259
@override
212260
Widget build(BuildContext context) {
213261
final isActive = current.startsWith(route);
214-
return Tooltip(
215-
message: label,
216-
preferBelow: false,
217-
child: InkWell(
218-
onTap: () => context.go(route),
219-
borderRadius: BorderRadius.circular(12),
220-
child: Container(
221-
width: 48,
222-
height: 48,
223-
margin: const EdgeInsets.symmetric(vertical: 4),
224-
decoration: BoxDecoration(
225-
gradient: isActive
226-
? const LinearGradient(
227-
colors: [Color(0xFF6366F1), Color(0xFF8B5CF6)],
228-
begin: Alignment.topLeft,
229-
end: Alignment.bottomRight,
230-
)
231-
: null,
232-
borderRadius: BorderRadius.circular(12),
233-
),
234-
child: Icon(
235-
icon,
236-
size: 22,
237-
color: isActive
238-
? Colors.white
239-
: const Color(0xFF908FA0),
262+
return Semantics(
263+
label: 'Navigation to $label',
264+
button: true,
265+
enabled: true,
266+
child: Tooltip(
267+
message: label,
268+
preferBelow: false,
269+
child: InkWell(
270+
onTap: () => context.go(route),
271+
borderRadius: BorderRadius.circular(12),
272+
child: Container(
273+
width: 48,
274+
height: 48,
275+
margin: const EdgeInsets.symmetric(vertical: 4),
276+
decoration: BoxDecoration(
277+
gradient: isActive
278+
? const LinearGradient(
279+
colors: [Color(0xFF6366F1), Color(0xFF8B5CF6)],
280+
begin: Alignment.topLeft,
281+
end: Alignment.bottomRight,
282+
)
283+
: null,
284+
borderRadius: BorderRadius.circular(12),
285+
),
286+
child: Icon(
287+
icon,
288+
size: 22,
289+
color: isActive
290+
? Colors.white
291+
: const Color(0xFF908FA0),
292+
),
240293
),
241294
),
242295
),
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import 'package:firebase_auth/firebase_auth.dart';
2+
import 'package:google_sign_in/google_sign_in.dart';
3+
import 'package:flutter/foundation.dart';
4+
5+
class AuthService {
6+
final FirebaseAuth _auth = FirebaseAuth.instance;
7+
final GoogleSignIn _googleSignIn = GoogleSignIn();
8+
9+
// Sign in with Google
10+
Future<User?> signInWithGoogle() async {
11+
try {
12+
if (kIsWeb) {
13+
// Web flow
14+
GoogleAuthProvider googleProvider = GoogleAuthProvider();
15+
final UserCredential result = await _auth.signInWithPopup(googleProvider);
16+
return result.user;
17+
} else {
18+
// Mobile flow
19+
final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
20+
if (googleUser == null) return null;
21+
22+
final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
23+
final AuthCredential credential = GoogleAuthProvider.credential(
24+
accessToken: googleAuth.accessToken,
25+
idToken: googleAuth.idToken,
26+
);
27+
28+
final UserCredential result = await _auth.signInWithCredential(credential);
29+
return result.user;
30+
}
31+
} catch (e) {
32+
debugPrint('Google Sign-In Error: $e');
33+
return null;
34+
}
35+
}
36+
37+
// Sign in anonymously (Legacy/Guest)
38+
Future<User?> signInAnonymously() async {
39+
try {
40+
UserCredential result = await _auth.signInAnonymously();
41+
return result.user;
42+
} catch (e) {
43+
debugPrint('Auth Error: $e');
44+
return null;
45+
}
46+
}
47+
48+
// Get current user
49+
User? get currentUser => _auth.currentUser;
50+
51+
// Get current UID
52+
String? get currentUid => _auth.currentUser?.uid;
53+
54+
// Sign out
55+
Future<void> signOut() async {
56+
try {
57+
await _googleSignIn.signOut();
58+
await _auth.signOut();
59+
} catch (e) {
60+
debugPrint('Logout Error: $e');
61+
}
62+
}
63+
}
64+

0 commit comments

Comments
 (0)