Files
Eventify-frontend/lib/features/auth/services/auth_service.dart
Sicherhaven fe8af7cfe6 feat: Phase 2 — 11 high-priority gaps implemented across home, auth, gamification, profile, and event detail
Phase 2 gaps completed:
- HOME-001: Hero slider pause-on-touch (GestureDetector wraps PageView)
- HOME-003: Calendar bottom sheet with TableCalendar (replaces custom dialog)
- AUTH-004: District dropdown on signup (14 Kerala districts)
- EVT-003: Mobile sticky "Book Now" bar + desktop CTA wired to CheckoutScreen
- ACH-001: Real achievements from dashboard API with fallback defaults
- GAM-002: 3-card EP row (Lifetime EP / Liquid EP / Reward Points)
- GAM-005: Horizontal tier roadmap Bronze→Silver→Gold→Platinum→Diamond
- CTR-001: Submission status chips (PENDING/APPROVED/REJECTED)
- CTR-002: +EP badge on approved submissions
- PROF-003: Gamification cards on profile screen with Consumer<GamificationProvider>
- UX-001: Shimmer skeleton loaders (shimmer ^3.0.0) replacing CircularProgressIndicator

Already complete (verified, no changes needed):
- HOME-002: Category shelves already built in _buildTypeSection()
- LDR-002: Podium visualization already built (_buildPodium / _buildDesktopPodium)
- BOOK-004: UPI handled natively by Razorpay SDK

Deferred: LOC-001/002 (needs Django haversine endpoint)
Skipped: AUTH-002 (OTP needs SMS provider decision)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 16:51:30 +05:30

212 lines
7.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// lib/features/auth/services/auth_service.dart
import 'package:flutter/foundation.dart' show kDebugMode, debugPrint;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:google_sign_in/google_sign_in.dart';
import '../../../core/api/api_client.dart';
import '../../../core/api/api_endpoints.dart';
import '../../../core/auth/auth_guard.dart';
import '../../../core/storage/token_storage.dart';
import '../../../core/analytics/posthog_service.dart';
import '../models/user_model.dart';
class AuthService {
final ApiClient _api = ApiClient();
/// LOGIN → returns UserModel
Future<UserModel> login(String username, String password) async {
try {
final res = await _api.post(
ApiEndpoints.login,
body: {
"username": username,
"password": password,
},
requiresAuth: false,
);
final token = res['token'];
if (token == null) {
throw Exception('Token missing from response');
}
final serverUsername = (res['username'] is String && (res['username'] as String).isNotEmpty) ? res['username'] as String : null;
final serverEmail = (res['email'] is String && (res['email'] as String).isNotEmpty) ? res['email'] as String : null;
// savedEmail is the canonical identifier for this account
final savedEmail = serverEmail ?? username;
// candidate display name (server username or email fallback)
final displayCandidate = serverUsername ?? savedEmail;
// clear guest mode on successful login
AuthGuard.setGuest(false);
// save token (TokenStorage stays responsible for token)
await TokenStorage.saveToken(token.toString(), savedEmail);
// persist per-account fields in SharedPreferences
final prefs = await SharedPreferences.getInstance();
// mark current logged-in account
await prefs.setString('current_email', savedEmail);
// always keep a canonical 'email' pointing to current user's email for old callers
await prefs.setString('email', savedEmail);
// save per-account display name (do not overwrite an explicit per-account display name if already set)
final perKey = 'display_name_$savedEmail';
final existingPer = prefs.getString(perKey);
if (existingPer == null || existingPer.trim().isEmpty) {
await prefs.setString(perKey, displayCandidate);
}
// save server-provided email (if any) separately too
if (serverEmail != null) await prefs.setString('server_email', serverEmail);
// Save phone if provided (optional)
if (res['phone_number'] != null) await prefs.setString('phone_number', res['phone_number'].toString());
PostHogService.instance.identify(savedEmail, properties: {
'username': displayCandidate,
'login_method': 'email',
});
PostHogService.instance.capture('user_logged_in');
return UserModel.fromJson(res);
} catch (e) {
if (kDebugMode) debugPrint('AuthService.login error: $e');
rethrow;
}
}
/// REGISTER → returns UserModel
Future<UserModel> register({
required String email,
required String phoneNumber,
required String password,
String? district,
}) async {
try {
final body = <String, dynamic>{
"email": email,
"phone_number": phoneNumber,
"password": password,
};
if (district != null && district.isNotEmpty) {
body["district"] = district;
}
final res = await _api.post(
ApiEndpoints.register,
body: body,
requiresAuth: false,
);
final token = res['token'];
if (token == null) {
throw Exception('Token missing from response');
}
final serverUsername = (res['username'] is String && (res['username'] as String).isNotEmpty) ? res['username'] as String : null;
final serverEmail = (res['email'] is String && (res['email'] as String).isNotEmpty) ? res['email'] as String : null;
final savedEmail = serverEmail ?? email;
final savedUsername = serverUsername ?? savedEmail;
final savedRole = (res['role'] ?? 'user').toString();
final savedPhone = (res['phone_number'] ?? phoneNumber)?.toString();
// clear guest mode on successful registration
AuthGuard.setGuest(false);
// Save token + canonical user id for token storage
await TokenStorage.saveToken(token.toString(), savedEmail);
// Persist per-account keys
final prefs = await SharedPreferences.getInstance();
await prefs.setString('current_email', savedEmail);
await prefs.setString('email', savedEmail);
// Set display_name_<email> if not present yet
final perKey = 'display_name_$savedEmail';
final existing = prefs.getString(perKey);
if (existing == null || existing.trim().isEmpty) {
await prefs.setString(perKey, savedUsername);
}
// Optionally save phone/email in prefs
await prefs.setString('server_email', savedEmail);
if (savedPhone != null) await prefs.setString('phone_number', savedPhone);
// Return a simple UserModel (defensive)
return UserModel(
username: savedUsername,
email: savedEmail,
role: savedRole,
token: token.toString(),
phoneNumber: savedPhone,
);
} catch (e) {
if (kDebugMode) debugPrint('AuthService.register error: $e');
rethrow;
}
}
/// GOOGLE OAUTH LOGIN → returns UserModel
Future<UserModel> googleLogin() async {
try {
final googleSignIn = GoogleSignIn(scopes: ['email']);
final account = await googleSignIn.signIn();
if (account == null) throw Exception('Google sign-in cancelled');
final auth = await account.authentication;
final idToken = auth.idToken;
if (idToken == null) throw Exception('Failed to get Google ID token');
final res = await _api.post(
ApiEndpoints.googleLogin,
body: {'id_token': idToken},
requiresAuth: false,
);
final token = res['token'];
if (token == null) throw Exception('Token missing from response');
final serverEmail = (res['email'] as String?) ?? account.email;
final displayName = (res['username'] as String?) ?? account.displayName ?? serverEmail;
AuthGuard.setGuest(false);
await TokenStorage.saveToken(token.toString(), serverEmail);
final prefs = await SharedPreferences.getInstance();
await prefs.setString('current_email', serverEmail);
await prefs.setString('email', serverEmail);
final perKey = 'display_name_$serverEmail';
if ((prefs.getString(perKey) ?? '').isEmpty) {
await prefs.setString(perKey, displayName);
}
if (res['phone_number'] != null) await prefs.setString('phone_number', res['phone_number'].toString());
PostHogService.instance.identify(serverEmail, properties: {
'username': displayName,
'login_method': 'google',
});
PostHogService.instance.capture('user_logged_in');
return UserModel.fromJson(res);
} catch (e) {
if (kDebugMode) debugPrint('AuthService.googleLogin error: $e');
rethrow;
}
}
/// Logout clear auth token and current_email (keep per-account display_name entries so they persist)
Future<void> logout() async {
try {
final prefs = await SharedPreferences.getInstance();
// clear token storage (should delete token + auth headers)
await TokenStorage.clear();
// remove current_email marker so no previous account remains current
await prefs.remove('current_email');
// Also remove canonical 'email' pointing to current user
await prefs.remove('email');
// Do not delete display_name_<email> entries — they are per-account and should remain on device.
PostHogService.instance.capture('user_logged_out');
PostHogService.instance.reset();
} catch (e) {
if (kDebugMode) debugPrint('AuthService.logout warning: $e');
}
}
}