fix: v2.0.4+24 — login fixes, signup toggle, forgot-password, guest SnackBar, Google OAuth

- Google Sign-In: wire serverClientId (639347358523-mtkm...apps.googleusercontent.com) so idToken is returned on Android
- Email login: raise timeout 10s→25s, add single retry on SocketException/TimeoutException
- Forgot Password: real glassmorphism bottom sheet with safe-degrade SnackBar (endpoint missing on backend)
- Create Account: same-page AnimatedSwitcher toggle with glassmorphism signup form; delete old RegisterScreen
- Desktop parity: DesktopLoginScreen same-page toggle; delete DesktopRegisterScreen
- Guest mode: remove ScaffoldMessenger SnackBar from HomeScreen outer catch (inner _safe wrappers already return [])
- LoginScreen: clearSnackBars() on postFrameCallback to prevent carried-over SnackBars from prior screens
- ProGuard: add Google Sign-In + OkHttp keep rules
- Version bump: 2.0.0+20 → 2.0.4+24; settings _appVersion → 2.0.4

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-19 21:40:17 +05:30
parent 5e00e431e3
commit ebe654f9c3
9 changed files with 879 additions and 368 deletions

View File

@@ -26,3 +26,62 @@
-dontwarn com.google.android.play.core.tasks.OnFailureListener -dontwarn com.google.android.play.core.tasks.OnFailureListener
-dontwarn com.google.android.play.core.tasks.OnSuccessListener -dontwarn com.google.android.play.core.tasks.OnSuccessListener
-dontwarn com.google.android.play.core.tasks.Task -dontwarn com.google.android.play.core.tasks.Task
# Razorpay
-keepattributes *Annotation*,Signature,*Annotation*
-dontwarn com.razorpay.**
-keep class com.razorpay.** { *; }
-optimizations !method/inlining/
-keepclasseswithmembers class * {
public void onPayment*(...);
}
-keep class proguard.annotation.Keep
-keep class proguard.annotation.KeepClassMembers
-keep @proguard.annotation.Keep class * { *; }
-keep @proguard.annotation.KeepClassMembers class * {
<fields>;
<methods>;
}
# Google Sign-In / Play Services
-keep class com.google.android.gms.** { *; }
-keep interface com.google.android.gms.** { *; }
-dontwarn com.google.android.gms.**
-keep class com.google.firebase.** { *; }
-dontwarn com.google.firebase.**
# Geolocator / Geocoding
-keep class com.baseflow.** { *; }
-dontwarn com.baseflow.**
# url_launcher, share_plus, image_picker, path_provider, etc.
-keep class io.flutter.plugins.** { *; }
-dontwarn io.flutter.plugins.**
# OkHttp (used by many network libs)
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn javax.annotation.**
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
# Keep native methods
-keepclasseswithmembernames class * {
native <methods>;
}
# Keep Parcelable classes
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
# Keep Serializable classes
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient <fields>;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}

View File

@@ -1,12 +1,15 @@
// lib/core/api/api_client.dart // lib/core/api/api_client.dart
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io' show SocketException;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart'; import 'package:http_parser/http_parser.dart';
import '../storage/token_storage.dart'; import '../storage/token_storage.dart';
class ApiClient { class ApiClient {
static const Duration _timeout = Duration(seconds: 10); static const Duration _timeout = Duration(seconds: 25);
static const Duration _retryDelay = Duration(milliseconds: 600);
// Set to true to enable mock/offline development mode (useful when backend is unavailable) // Set to true to enable mock/offline development mode (useful when backend is unavailable)
static const bool _developmentMode = false; static const bool _developmentMode = false;
@@ -28,13 +31,7 @@ class ApiClient {
late http.Response response; late http.Response response;
try { try {
response = await http response = await _postWithRetry(url, headers, finalBody);
.post(
Uri.parse(url),
headers: headers,
body: jsonEncode(finalBody),
)
.timeout(_timeout);
} catch (e) { } catch (e) {
if (kDebugMode) debugPrint('ApiClient.post network error: $e'); if (kDebugMode) debugPrint('ApiClient.post network error: $e');
@@ -100,6 +97,32 @@ class ApiClient {
return _handleResponse(url, response, finalBody); return _handleResponse(url, response, finalBody);
} }
/// POST with one retry on transient network errors.
/// Retries on SocketException / TimeoutException only.
Future<http.Response> _postWithRetry(
String url,
Map<String, String> headers,
Map<String, dynamic> body,
) async {
try {
return await http
.post(Uri.parse(url), headers: headers, body: jsonEncode(body))
.timeout(_timeout);
} on SocketException {
if (kDebugMode) debugPrint('ApiClient.post retry after SocketException');
await Future.delayed(_retryDelay);
return await http
.post(Uri.parse(url), headers: headers, body: jsonEncode(body))
.timeout(_timeout);
} on TimeoutException {
if (kDebugMode) debugPrint('ApiClient.post retry after TimeoutException');
await Future.delayed(_retryDelay);
return await http
.post(Uri.parse(url), headers: headers, body: jsonEncode(body))
.timeout(_timeout);
}
}
/// Upload a single file as multipart/form-data. /// Upload a single file as multipart/form-data.
/// ///
/// Returns the `file` object from the server response: /// Returns the `file` object from the server response:

View File

@@ -15,6 +15,7 @@ class ApiEndpoints {
static const String logout = "$baseUrl/user/logout/"; static const String logout = "$baseUrl/user/logout/";
static const String status = "$baseUrl/user/status/"; static const String status = "$baseUrl/user/status/";
static const String updateProfile = "$baseUrl/user/update-profile/"; static const String updateProfile = "$baseUrl/user/update-profile/";
static const String forgotPassword = "$baseUrl/user/forgot-password/";
// Events // Events
static const String eventTypes = "$baseUrl/events/type-list/"; // list of event types static const String eventTypes = "$baseUrl/events/type-list/"; // list of event types

View File

@@ -12,6 +12,13 @@ import '../models/user_model.dart';
class AuthService { class AuthService {
final ApiClient _api = ApiClient(); final ApiClient _api = ApiClient();
/// Google OAuth 2.0 Web Client ID from Google Cloud Console.
/// Must match the `GOOGLE_CLIENT_ID` env var set on the Django backend
/// so the server can verify the `id_token` audience.
/// Source: Google Cloud Console → APIs & Services → Credentials → Web application.
static const String _googleWebClientId =
'639347358523-mtkm3i8vssuhsun80rp2llt09eou0p8g.apps.googleusercontent.com';
/// LOGIN → returns UserModel /// LOGIN → returns UserModel
Future<UserModel> login(String username, String password) async { Future<UserModel> login(String username, String password) async {
try { try {
@@ -158,7 +165,10 @@ class AuthService {
/// GOOGLE OAUTH LOGIN → returns UserModel /// GOOGLE OAUTH LOGIN → returns UserModel
Future<UserModel> googleLogin() async { Future<UserModel> googleLogin() async {
try { try {
final googleSignIn = GoogleSignIn(scopes: ['email']); final googleSignIn = GoogleSignIn(
scopes: const ['email', 'profile'],
serverClientId: _googleWebClientId,
);
final account = await googleSignIn.signIn(); final account = await googleSignIn.signIn();
if (account == null) throw Exception('Google sign-in cancelled'); if (account == null) throw Exception('Google sign-in cancelled');
@@ -215,6 +225,16 @@ class AuthService {
} }
} }
/// FORGOT PASSWORD → backend sends reset instructions by email.
/// Frontend never leaks whether the email is registered — same UX on success and 404.
Future<void> forgotPassword(String email) async {
await _api.post(
ApiEndpoints.forgotPassword,
body: {'email': email},
requiresAuth: false,
);
}
/// Logout clear auth token and current_email (keep per-account display_name entries so they persist) /// Logout clear auth token and current_email (keep per-account display_name entries so they persist)
Future<void> logout() async { Future<void> logout() async {
try { try {

View File

@@ -15,9 +15,23 @@ class DesktopLoginScreen extends StatefulWidget {
} }
class _DesktopLoginScreenState extends State<DesktopLoginScreen> with SingleTickerProviderStateMixin { class _DesktopLoginScreenState extends State<DesktopLoginScreen> with SingleTickerProviderStateMixin {
// Login controllers
final TextEditingController _emailCtrl = TextEditingController(); final TextEditingController _emailCtrl = TextEditingController();
final TextEditingController _passCtrl = TextEditingController(); final TextEditingController _passCtrl = TextEditingController();
// Signup controllers
final TextEditingController _signupEmailCtrl = TextEditingController();
final TextEditingController _signupPhoneCtrl = TextEditingController();
final TextEditingController _signupPassCtrl = TextEditingController();
final TextEditingController _signupConfirmCtrl = TextEditingController();
String? _signupDistrict;
static const _districts = [
'Thiruvananthapuram', 'Kollam', 'Pathanamthitta', 'Alappuzha',
'Kottayam', 'Idukki', 'Ernakulam', 'Thrissur', 'Palakkad',
'Malappuram', 'Kozhikode', 'Wayanad', 'Kannur', 'Kasaragod',
];
final AuthService _auth = AuthService(); final AuthService _auth = AuthService();
AnimationController? _controller; AnimationController? _controller;
@@ -31,13 +45,18 @@ class _DesktopLoginScreenState extends State<DesktopLoginScreen> with SingleTick
final Curve _curve = Curves.easeInOutCubic; final Curve _curve = Curves.easeInOutCubic;
bool _isAnimating = false; bool _isAnimating = false;
bool _loading = false; // network loading flag bool _loading = false;
bool _isSignupMode = false;
@override @override
void dispose() { void dispose() {
_controller?.dispose(); _controller?.dispose();
_emailCtrl.dispose(); _emailCtrl.dispose();
_passCtrl.dispose(); _passCtrl.dispose();
_signupEmailCtrl.dispose();
_signupPhoneCtrl.dispose();
_signupPassCtrl.dispose();
_signupConfirmCtrl.dispose();
super.dispose(); super.dispose();
} }
@@ -52,7 +71,6 @@ class _DesktopLoginScreenState extends State<DesktopLoginScreen> with SingleTick
_leftTextOpacityAnim = Tween<double>(begin: 1.0, end: 0.0).animate( _leftTextOpacityAnim = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(parent: _controller!, curve: const Interval(0.0, 0.35, curve: Curves.easeOut)), CurvedAnimation(parent: _controller!, curve: const Interval(0.0, 0.35, curve: Curves.easeOut)),
); );
_formOpacityAnim = Tween<double>(begin: 1.0, end: 0.0).animate( _formOpacityAnim = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(parent: _controller!, curve: const Interval(0.0, 0.55, curve: Curves.easeOut)), CurvedAnimation(parent: _controller!, curve: const Interval(0.0, 0.55, curve: Curves.easeOut)),
); );
@@ -68,9 +86,7 @@ class _DesktopLoginScreenState extends State<DesktopLoginScreen> with SingleTick
Future<void> _performLoginFlow(double initialLeftWidth) async { Future<void> _performLoginFlow(double initialLeftWidth) async {
if (_isAnimating || _loading) return; if (_isAnimating || _loading) return;
setState(() { setState(() => _loading = true);
_loading = true;
});
final email = _emailCtrl.text.trim(); final email = _emailCtrl.text.trim();
final password = _passCtrl.text; final password = _passCtrl.text;
@@ -87,14 +103,9 @@ class _DesktopLoginScreenState extends State<DesktopLoginScreen> with SingleTick
} }
try { try {
// Capture user model returned by AuthService (AuthService already saves prefs)
await _auth.login(email, password); await _auth.login(email, password);
// on success run opening animation
await _startCollapseAnimation(initialLeftWidth); await _startCollapseAnimation(initialLeftWidth);
if (!mounted) return; if (!mounted) return;
Navigator.of(context).pushReplacement(PageRouteBuilder( Navigator.of(context).pushReplacement(PageRouteBuilder(
pageBuilder: (context, a1, a2) => const HomeDesktopScreen(skipSidebarEntranceAnimation: true), pageBuilder: (context, a1, a2) => const HomeDesktopScreen(skipSidebarEntranceAnimation: true),
transitionDuration: Duration.zero, transitionDuration: Duration.zero,
@@ -102,90 +113,135 @@ class _DesktopLoginScreenState extends State<DesktopLoginScreen> with SingleTick
)); ));
} catch (e) { } catch (e) {
if (!mounted) return; if (!mounted) return;
final message = userFriendlyError(e); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e))));
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
setState(() => _isAnimating = false); setState(() => _isAnimating = false);
} finally { } finally {
if (mounted) setState(() { if (mounted) setState(() => _loading = false);
_loading = false;
});
} }
} }
void _openRegister() { Future<void> _performSignupFlow(double initialLeftWidth) async {
Navigator.of(context).push(MaterialPageRoute(builder: (_) => const DesktopRegisterScreen())); if (_isAnimating || _loading) return;
final email = _signupEmailCtrl.text.trim();
final phone = _signupPhoneCtrl.text.trim();
final pass = _signupPassCtrl.text;
final confirm = _signupConfirmCtrl.text;
if (email.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Enter email')));
return;
}
if (phone.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Enter phone number')));
return;
}
if (pass.length < 6) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Password must be at least 6 characters')));
return;
}
if (pass != confirm) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Passwords do not match')));
return;
} }
@override setState(() => _loading = true);
Widget build(BuildContext context) {
final screenW = MediaQuery.of(context).size.width;
final double safeInitialWidth = (screenW * 0.45).clamp(360.0, screenW * 0.65); try {
final bool animAvailable = _controller != null && _leftWidthAnim != null; await _auth.register(
final Listenable animation = _controller ?? AlwaysStoppedAnimation(0.0); email: email,
phoneNumber: phone,
password: pass,
district: _signupDistrict,
);
await _startCollapseAnimation(initialLeftWidth);
if (!mounted) return;
Navigator.of(context).pushReplacement(PageRouteBuilder(
pageBuilder: (context, a1, a2) => const HomeDesktopScreen(skipSidebarEntranceAnimation: true),
transitionDuration: Duration.zero,
reverseTransitionDuration: Duration.zero,
));
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e))));
setState(() => _isAnimating = false);
} finally {
if (mounted) setState(() => _loading = false);
}
}
return Scaffold( Future<void> _openForgotPasswordDialog() async {
body: SafeArea( final emailCtrl = TextEditingController(text: _emailCtrl.text.trim());
child: AnimatedBuilder( bool submitting = false;
animation: animation,
builder: (context, child) {
final leftWidth = animAvailable ? _leftWidthAnim!.value : safeInitialWidth;
final leftTextOpacity = animAvailable && _leftTextOpacityAnim != null ? _leftTextOpacityAnim!.value : 1.0;
final formOpacity = animAvailable && _formOpacityAnim != null ? _formOpacityAnim!.value : 1.0;
final formOffset = animAvailable && _formOffsetAnim != null ? _formOffsetAnim!.value : 0.0;
return Row( await showDialog<void>(
children: [ context: context,
Container( builder: (ctx) {
width: leftWidth, return StatefulBuilder(
height: double.infinity, builder: (ctx, setDialog) {
// color: const Color(0xFF0B63D6), return AlertDialog(
decoration: AppDecoration.blueGradient, title: const Text('Forgot Password'),
padding: const EdgeInsets.symmetric(horizontal: 36, vertical: 28), content: SizedBox(
child: Opacity( width: 360,
opacity: leftTextOpacity,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 4), const Text("Enter your email and we'll send reset instructions."),
const Text('EVENTIFY', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 22)),
const Spacer(),
const Text('Welcome Back!', style: TextStyle(color: Colors.white, fontSize: 34, fontWeight: FontWeight.bold)),
const SizedBox(height: 12), const SizedBox(height: 12),
const Text( TextField(
'Sign in to access your dashboard, manage events, and stay connected.', controller: emailCtrl,
style: TextStyle(color: Colors.white70, fontSize: 14), decoration: InputDecoration(
), prefixIcon: const Icon(Icons.email),
const Spacer(flex: 2), labelText: 'Email',
Opacity( border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
opacity: leftWidth > (_collapsedWidth + 8) ? 1.0 : 0.0,
child: const Padding(
padding: EdgeInsets.only(bottom: 12.0),
child: Text('© Eventify', style: TextStyle(color: Colors.white54)),
), ),
keyboardType: TextInputType.emailAddress,
), ),
], ],
), ),
), ),
actions: [
TextButton(onPressed: () => Navigator.of(ctx).pop(), child: const Text('Cancel')),
ElevatedButton(
onPressed: submitting
? null
: () async {
final email = emailCtrl.text.trim();
if (email.isEmpty) return;
setDialog(() => submitting = true);
try {
await _auth.forgotPassword(email);
} catch (_) {
// safe-degrade
}
if (!ctx.mounted) return;
Navigator.of(ctx).pop();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("If that email is registered, we've sent reset instructions."),
duration: Duration(seconds: 4),
), ),
);
},
child: submitting
? const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2))
: const Text('Send reset link'),
),
],
);
},
);
},
);
Expanded( emailCtrl.dispose();
child: Transform.translate( }
offset: Offset(formOffset, 0),
child: Opacity( Widget _buildLoginFields(double safeInitialWidth) {
opacity: formOpacity, return Column(
child: Container( key: const ValueKey('login'),
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 36),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: Card(
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 28.0, vertical: 28.0),
child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
@@ -217,21 +273,16 @@ class _DesktopLoginScreenState extends State<DesktopLoginScreen> with SingleTick
children: [ children: [
Row(children: [ Row(children: [
Checkbox(value: true, onChanged: (_) {}), Checkbox(value: true, onChanged: (_) {}),
const Text('Remember me') const Text('Remember me'),
]), ]),
TextButton(onPressed: () {}, child: const Text('Forgot Password?')) TextButton(onPressed: _openForgotPasswordDialog, child: const Text('Forgot Password?')),
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
SizedBox( SizedBox(
height: 50, height: 50,
child: ElevatedButton( child: ElevatedButton(
onPressed: (_isAnimating || _loading) onPressed: (_isAnimating || _loading) ? null : () => _performLoginFlow(safeInitialWidth),
? null
: () {
final double initial = safeInitialWidth;
_performLoginFlow(initial);
},
style: ElevatedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), style: ElevatedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
child: (_isAnimating || _loading) child: (_isAnimating || _loading)
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
@@ -242,7 +293,10 @@ class _DesktopLoginScreenState extends State<DesktopLoginScreen> with SingleTick
Wrap( Wrap(
alignment: WrapAlignment.spaceBetween, alignment: WrapAlignment.spaceBetween,
children: [ children: [
TextButton(onPressed: _openRegister, child: const Text("Don't have an account? Register")), TextButton(
onPressed: () => setState(() => _isSignupMode = true),
child: const Text("Don't have an account? Register"),
),
TextButton(onPressed: () {}, child: const Text('Contact support')), TextButton(onPressed: () {}, child: const Text('Contact support')),
TextButton( TextButton(
onPressed: () { onPressed: () {
@@ -255,8 +309,171 @@ class _DesktopLoginScreenState extends State<DesktopLoginScreen> with SingleTick
child: const Text('Continue as Guest'), child: const Text('Continue as Guest'),
), ),
], ],
) ),
], ],
);
}
Widget _buildSignupFields(double safeInitialWidth) {
return Column(
key: const ValueKey('signup'),
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text('Create Account', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), textAlign: TextAlign.center),
const SizedBox(height: 6),
const Text('Fill in your details to get started', textAlign: TextAlign.center, style: TextStyle(color: Colors.black54)),
const SizedBox(height: 22),
TextField(
controller: _signupEmailCtrl,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.email),
labelText: 'Email Address',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
),
const SizedBox(height: 12),
TextField(
controller: _signupPhoneCtrl,
keyboardType: TextInputType.phone,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.phone),
labelText: 'Phone Number',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
value: _signupDistrict,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.location_on),
labelText: 'District (optional)',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
items: _districts.map((d) => DropdownMenuItem(value: d, child: Text(d))).toList(),
onChanged: (v) => setState(() => _signupDistrict = v),
),
const SizedBox(height: 12),
TextField(
controller: _signupPassCtrl,
obscureText: true,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock),
labelText: 'Password',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
),
const SizedBox(height: 12),
TextField(
controller: _signupConfirmCtrl,
obscureText: true,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock_outline),
labelText: 'Confirm Password',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
),
),
const SizedBox(height: 16),
SizedBox(
height: 50,
child: ElevatedButton(
onPressed: (_isAnimating || _loading) ? null : () => _performSignupFlow(safeInitialWidth),
style: ElevatedButton.styleFrom(shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))),
child: (_isAnimating || _loading)
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
: const Text('Create Account', style: TextStyle(fontSize: 16)),
),
),
const SizedBox(height: 12),
Center(
child: TextButton(
onPressed: () => setState(() => _isSignupMode = false),
child: const Text('Already have an account? Sign in'),
),
),
],
);
}
@override
Widget build(BuildContext context) {
final screenW = MediaQuery.of(context).size.width;
final double safeInitialWidth = (screenW * 0.45).clamp(360.0, screenW * 0.65);
final bool animAvailable = _controller != null && _leftWidthAnim != null;
final Listenable animation = _controller ?? AlwaysStoppedAnimation(0.0);
return Scaffold(
body: SafeArea(
child: AnimatedBuilder(
animation: animation,
builder: (context, child) {
final leftWidth = animAvailable ? _leftWidthAnim!.value : safeInitialWidth;
final leftTextOpacity = animAvailable && _leftTextOpacityAnim != null ? _leftTextOpacityAnim!.value : 1.0;
final formOpacity = animAvailable && _formOpacityAnim != null ? _formOpacityAnim!.value : 1.0;
final formOffset = animAvailable && _formOffsetAnim != null ? _formOffsetAnim!.value : 0.0;
return Row(
children: [
Container(
width: leftWidth,
height: double.infinity,
decoration: AppDecoration.blueGradient,
padding: const EdgeInsets.symmetric(horizontal: 36, vertical: 28),
child: Opacity(
opacity: leftTextOpacity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
const Text('EVENTIFY', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 22)),
const Spacer(),
Text(
_isSignupMode ? 'Join Eventify!' : 'Welcome Back!',
style: const TextStyle(color: Colors.white, fontSize: 34, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Text(
_isSignupMode
? 'Create your account to discover events, book tickets, and connect with your community.'
: 'Sign in to access your dashboard, manage events, and stay connected.',
style: const TextStyle(color: Colors.white70, fontSize: 14),
),
const Spacer(flex: 2),
Opacity(
opacity: leftWidth > (_collapsedWidth + 8) ? 1.0 : 0.0,
child: const Padding(
padding: EdgeInsets.only(bottom: 12.0),
child: Text('© Eventify', style: TextStyle(color: Colors.white54)),
),
),
],
),
),
),
Expanded(
child: Transform.translate(
offset: Offset(formOffset, 0),
child: Opacity(
opacity: formOpacity,
child: Container(
color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 36),
child: Center(
child: SingleChildScrollView(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: Card(
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 28.0, vertical: 28.0),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 260),
child: _isSignupMode
? _buildSignupFields(safeInitialWidth)
: _buildLoginFields(safeInitialWidth),
),
), ),
), ),
), ),
@@ -274,113 +491,3 @@ class _DesktopLoginScreenState extends State<DesktopLoginScreen> with SingleTick
); );
} }
} }
class DesktopRegisterScreen extends StatefulWidget {
const DesktopRegisterScreen({Key? key}) : super(key: key);
@override
State<DesktopRegisterScreen> createState() => _DesktopRegisterScreenState();
}
class _DesktopRegisterScreenState extends State<DesktopRegisterScreen> {
final TextEditingController _emailCtrl = TextEditingController();
final TextEditingController _phoneCtrl = TextEditingController();
final TextEditingController _passCtrl = TextEditingController();
final TextEditingController _confirmCtrl = TextEditingController();
final AuthService _auth = AuthService();
bool _loading = false;
@override
void dispose() {
_emailCtrl.dispose();
_phoneCtrl.dispose();
_passCtrl.dispose();
_confirmCtrl.dispose();
super.dispose();
}
Future<void> _performRegister() async {
final email = _emailCtrl.text.trim();
final phone = _phoneCtrl.text.trim();
final pass = _passCtrl.text;
final confirm = _confirmCtrl.text;
if (email.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Enter email')));
return;
}
if (phone.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Enter phone number')));
return;
}
if (pass.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Enter password')));
return;
}
if (pass != confirm) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Passwords do not match')));
return;
}
setState(() => _loading = true);
try {
await _auth.register(
email: email,
phoneNumber: phone,
password: pass,
);
if (!mounted) return;
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const HomeDesktopScreen(skipSidebarEntranceAnimation: true)));
} catch (e) {
if (!mounted) return;
final message = userFriendlyError(e);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
} finally {
if (mounted) setState(() => _loading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Register')),
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: Card(
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Column(
children: [
TextField(controller: _emailCtrl, decoration: const InputDecoration(labelText: 'Email')),
const SizedBox(height: 8),
TextField(controller: _phoneCtrl, decoration: const InputDecoration(labelText: 'Phone')),
const SizedBox(height: 8),
TextField(controller: _passCtrl, obscureText: true, decoration: const InputDecoration(labelText: 'Password')),
const SizedBox(height: 8),
TextField(controller: _confirmCtrl, obscureText: true, decoration: const InputDecoration(labelText: 'Confirm password')),
const SizedBox(height: 16),
Row(
children: [
ElevatedButton(onPressed: _loading ? null : _performRegister, child: _loading ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : const Text('Register')),
const SizedBox(width: 12),
OutlinedButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel')),
],
)
],
),
),
),
),
),
),
),
);
}
}

View File

@@ -1,7 +1,7 @@
// lib/screens/home_screen.dart // lib/screens/home_screen.dart
import 'dart:async'; import 'dart:async';
import 'dart:ui'; import 'dart:ui';
import '../core/utils/error_utils.dart'; import 'package:flutter/foundation.dart' show kDebugMode, debugPrint;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../core/auth/auth_guard.dart'; import '../core/auth/auth_guard.dart';
@@ -137,10 +137,8 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
}); });
} }
} catch (e) { } catch (e) {
if (mounted) { if (kDebugMode) debugPrint('HomeScreen init unexpected error: $e');
setState(() => _loading = false); if (mounted) setState(() => _loading = false);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e))));
}
} }
// Refresh notification badge count (fire-and-forget) // Refresh notification badge count (fire-and-forget)

View File

@@ -2,7 +2,6 @@
import 'dart:ui'; import 'dart:ui';
import '../core/utils/error_utils.dart'; import '../core/utils/error_utils.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -22,12 +21,30 @@ class LoginScreen extends StatefulWidget {
class _LoginScreenState extends State<LoginScreen> { class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _signupFormKey = GlobalKey<FormState>();
final TextEditingController _emailCtrl = TextEditingController(); final TextEditingController _emailCtrl = TextEditingController();
final TextEditingController _passCtrl = TextEditingController(); final TextEditingController _passCtrl = TextEditingController();
final FocusNode _emailFocus = FocusNode(); final FocusNode _emailFocus = FocusNode();
final FocusNode _passFocus = FocusNode(); final FocusNode _passFocus = FocusNode();
// Signup-specific controllers
final TextEditingController _signupEmailCtrl = TextEditingController();
final TextEditingController _signupPhoneCtrl = TextEditingController();
final TextEditingController _signupPassCtrl = TextEditingController();
final TextEditingController _signupConfirmCtrl = TextEditingController();
String? _signupDistrict;
bool _signupObscurePass = true;
bool _signupObscureConfirm = true;
static const _districts = [
'Thiruvananthapuram', 'Kollam', 'Pathanamthitta', 'Alappuzha',
'Kottayam', 'Idukki', 'Ernakulam', 'Thrissur', 'Palakkad',
'Malappuram', 'Kozhikode', 'Wayanad', 'Kannur', 'Kasaragod',
];
bool _isSignupMode = false;
final AuthService _auth = AuthService(); final AuthService _auth = AuthService();
bool _loading = false; bool _loading = false;
bool _obscurePassword = true; bool _obscurePassword = true;
@@ -50,6 +67,9 @@ class _LoginScreenState extends State<LoginScreen> {
void initState() { void initState() {
super.initState(); super.initState();
_initVideo(); _initVideo();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) ScaffoldMessenger.of(context).clearSnackBars();
});
} }
Future<void> _initVideo() async { Future<void> _initVideo() async {
@@ -72,6 +92,10 @@ class _LoginScreenState extends State<LoginScreen> {
_passCtrl.dispose(); _passCtrl.dispose();
_emailFocus.dispose(); _emailFocus.dispose();
_passFocus.dispose(); _passFocus.dispose();
_signupEmailCtrl.dispose();
_signupPhoneCtrl.dispose();
_signupPassCtrl.dispose();
_signupConfirmCtrl.dispose();
super.dispose(); super.dispose();
} }
@@ -123,7 +147,11 @@ class _LoginScreenState extends State<LoginScreen> {
} }
void _openRegister() { void _openRegister() {
Navigator.of(context).push(MaterialPageRoute(builder: (_) => const RegisterScreen(isDesktop: false))); setState(() => _isSignupMode = true);
}
void _openLogin() {
setState(() => _isSignupMode = false);
} }
void _showComingSoon() { void _showComingSoon() {
@@ -132,6 +160,182 @@ class _LoginScreenState extends State<LoginScreen> {
); );
} }
Future<void> _performSignup() async {
if (!(_signupFormKey.currentState?.validate() ?? false)) return;
final email = _signupEmailCtrl.text.trim();
final phone = _signupPhoneCtrl.text.trim();
final pass = _signupPassCtrl.text;
final confirm = _signupConfirmCtrl.text;
if (pass != confirm) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Passwords do not match')));
return;
}
setState(() => _loading = true);
try {
await _auth.register(
email: email,
phoneNumber: phone,
password: pass,
district: _signupDistrict,
);
if (!mounted) return;
Navigator.of(context).pushReplacement(PageRouteBuilder(
pageBuilder: (context, a1, a2) => const HomeScreen(),
transitionDuration: const Duration(milliseconds: 650),
transitionsBuilder: (context, animation, _, child) => FadeTransition(opacity: animation, child: child),
));
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e))));
} finally {
if (mounted) setState(() => _loading = false);
}
}
Future<void> _openForgotPasswordSheet() async {
final emailCtrl = TextEditingController(text: _emailCtrl.text.trim());
final sheetFormKey = GlobalKey<FormState>();
bool submitting = false;
await showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) {
return StatefulBuilder(
builder: (ctx, setSheetState) {
return Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(ctx).viewInsets.bottom),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(top: Radius.circular(28)),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
padding: const EdgeInsets.fromLTRB(24, 20, 24, 28),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.75),
border: Border.all(color: _glassBorder, width: 0.8),
),
child: Form(
key: sheetFormKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.white24,
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 18),
const Text(
'Forgot Password',
style: TextStyle(color: _textWhite, fontSize: 20, fontWeight: FontWeight.w600),
),
const SizedBox(height: 6),
const Text(
"Enter your email and we'll send you reset instructions.",
style: TextStyle(color: _textMuted, fontSize: 13),
),
const SizedBox(height: 20),
TextFormField(
controller: emailCtrl,
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
style: const TextStyle(color: _textWhite, fontSize: 14),
cursorColor: Colors.white54,
decoration: _glassInputDecoration(
hint: 'Enter your email',
prefixIcon: Icons.mail_outline_rounded,
),
validator: _emailValidator,
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
gradient: const LinearGradient(
colors: [Color(0xFF2A2A2A), Color(0xFF1A1A1A)],
),
border: Border.all(color: const Color(0x33FFFFFF)),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(28),
onTap: submitting
? null
: () async {
if (!(sheetFormKey.currentState?.validate() ?? false)) return;
setSheetState(() => submitting = true);
final email = emailCtrl.text.trim();
try {
await _auth.forgotPassword(email);
} catch (_) {
// safe-degrade: don't leak whether email exists or backend status
}
if (!ctx.mounted) return;
Navigator.of(ctx).pop();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("If that email is registered, we've sent reset instructions."),
duration: Duration(seconds: 4),
),
);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: submitting
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: _textWhite),
)
: const Text(
'Send reset link',
style: TextStyle(color: _textWhite, fontSize: 16, fontWeight: FontWeight.w600),
),
),
),
),
),
),
),
const SizedBox(height: 12),
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('Cancel', style: TextStyle(color: _textMuted)),
),
],
),
),
),
),
),
);
},
);
},
);
emailCtrl.dispose();
}
Future<void> _performGoogleLogin() async { Future<void> _performGoogleLogin() async {
try { try {
setState(() => _loading = true); setState(() => _loading = true);
@@ -281,6 +485,14 @@ class _LoginScreenState extends State<LoginScreen> {
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20),
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400), constraints: const BoxConstraints(maxWidth: 400),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 280),
switchInCurve: Curves.easeOut,
switchOutCurve: Curves.easeIn,
child: _isSignupMode
? KeyedSubtree(key: const ValueKey('signup'), child: _buildSignupForm(context))
: KeyedSubtree(
key: const ValueKey('login'),
child: Form( child: Form(
key: _formKey, key: _formKey,
child: Column( child: Column(
@@ -410,7 +622,7 @@ class _LoginScreenState extends State<LoginScreen> {
), ),
// Forgot Password // Forgot Password
GestureDetector( GestureDetector(
onTap: _showComingSoon, onTap: _openForgotPasswordSheet,
child: const Text( child: const Text(
'Forgot Password?', 'Forgot Password?',
style: TextStyle(color: _textMuted, fontSize: 12), style: TextStyle(color: _textMuted, fontSize: 12),
@@ -575,148 +787,239 @@ class _LoginScreenState extends State<LoginScreen> {
), ),
), ),
), ),
),
),
], ],
), ),
); );
} }
}
/// Register screen calls backend register endpoint via AuthService.register Widget _buildSignupForm(BuildContext context) {
class RegisterScreen extends StatefulWidget { return Form(
final bool isDesktop; key: _signupFormKey,
const RegisterScreen({Key? key, this.isDesktop = false}) : super(key: key); child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
'Eventify',
style: TextStyle(
color: _textWhite.withOpacity(0.7),
fontSize: 16,
fontWeight: FontWeight.w500,
fontStyle: FontStyle.italic,
letterSpacing: 1.5,
),
),
),
const SizedBox(height: 12),
const Center(
child: Text(
'Create Your\nAccount',
textAlign: TextAlign.center,
style: TextStyle(
color: _textWhite,
fontSize: 28,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic,
height: 1.2,
),
),
),
const SizedBox(height: 28),
@override // Email
State<RegisterScreen> createState() => _RegisterScreenState(); const Padding(
} padding: EdgeInsets.only(left: 4, bottom: 8),
child: Text('Email', style: TextStyle(color: _textMuted, fontSize: 13)),
),
TextFormField(
controller: _signupEmailCtrl,
keyboardType: TextInputType.emailAddress,
autofillHints: const [AutofillHints.email],
style: const TextStyle(color: _textWhite, fontSize: 14),
cursorColor: Colors.white54,
decoration: _glassInputDecoration(
hint: 'Enter your email',
prefixIcon: Icons.mail_outline_rounded,
),
validator: _emailValidator,
textInputAction: TextInputAction.next,
),
const SizedBox(height: 16),
class _RegisterScreenState extends State<RegisterScreen> { // Phone
final _formKey = GlobalKey<FormState>(); const Padding(
final TextEditingController _emailCtrl = TextEditingController(); padding: EdgeInsets.only(left: 4, bottom: 8),
final TextEditingController _phoneCtrl = TextEditingController(); child: Text('Phone', style: TextStyle(color: _textMuted, fontSize: 13)),
final TextEditingController _passCtrl = TextEditingController(); ),
final TextEditingController _confirmCtrl = TextEditingController(); TextFormField(
final AuthService _auth = AuthService(); controller: _signupPhoneCtrl,
keyboardType: TextInputType.phone,
bool _loading = false; style: const TextStyle(color: _textWhite, fontSize: 14),
String? _selectedDistrict; cursorColor: Colors.white54,
decoration: _glassInputDecoration(
static const _districts = [ hint: 'Enter your phone number',
'Thiruvananthapuram', 'Kollam', 'Pathanamthitta', 'Alappuzha', prefixIcon: Icons.phone_outlined,
'Kottayam', 'Idukki', 'Ernakulam', 'Thrissur', 'Palakkad', ),
'Malappuram', 'Kozhikode', 'Wayanad', 'Kannur', 'Kasaragod', validator: (v) {
];
@override
void dispose() {
_emailCtrl.dispose();
_phoneCtrl.dispose();
_passCtrl.dispose();
_confirmCtrl.dispose();
super.dispose();
}
Future<void> _performRegister() async {
if (!(_formKey.currentState?.validate() ?? false)) return;
final email = _emailCtrl.text.trim();
final phone = _phoneCtrl.text.trim();
final pass = _passCtrl.text;
final confirm = _confirmCtrl.text;
if (pass != confirm) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Passwords do not match')));
return;
}
setState(() => _loading = true);
try {
await _auth.register(
email: email,
phoneNumber: phone,
password: pass,
district: _selectedDistrict,
);
if (!mounted) return;
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const HomeScreen()));
} catch (e) {
if (!mounted) return;
final message = userFriendlyError(e);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
} finally {
if (mounted) setState(() => _loading = false);
}
}
String? _emailValidator(String? v) {
if (v == null || v.trim().isEmpty) return 'Enter email';
final emailRegex = RegExp(r"^[^@]+@[^@]+\.[^@]+");
if (!emailRegex.hasMatch(v.trim())) return 'Enter a valid email';
return null;
}
String? _phoneValidator(String? v) {
if (v == null || v.trim().isEmpty) return 'Enter phone number'; if (v == null || v.trim().isEmpty) return 'Enter phone number';
if (v.trim().length < 7) return 'Enter a valid phone number'; if (v.trim().length < 7) return 'Enter a valid phone number';
return null; return null;
} },
textInputAction: TextInputAction.next,
String? _passwordValidator(String? v) {
if (v == null || v.isEmpty) return 'Enter password';
if (v.length < 6) return 'Password must be at least 6 characters';
return null;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Register')),
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: Card(
child: Padding(
padding: const EdgeInsets.all(18.0),
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(controller: _emailCtrl, decoration: const InputDecoration(labelText: 'Email'), validator: _emailValidator, keyboardType: TextInputType.emailAddress),
const SizedBox(height: 8),
TextFormField(controller: _phoneCtrl, decoration: const InputDecoration(labelText: 'Phone'), validator: _phoneValidator, keyboardType: TextInputType.phone),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
value: _selectedDistrict,
decoration: const InputDecoration(labelText: 'District (optional)'),
items: _districts.map((d) => DropdownMenuItem(value: d, child: Text(d))).toList(),
onChanged: (v) => setState(() => _selectedDistrict = v),
), ),
const SizedBox(height: 8),
TextFormField(controller: _passCtrl, obscureText: true, decoration: const InputDecoration(labelText: 'Password'), validator: _passwordValidator),
const SizedBox(height: 8),
TextFormField(controller: _confirmCtrl, obscureText: true, decoration: const InputDecoration(labelText: 'Confirm password'), validator: _passwordValidator),
const SizedBox(height: 16), const SizedBox(height: 16),
// District
const Padding(
padding: EdgeInsets.only(left: 4, bottom: 8),
child: Text('District (optional)', style: TextStyle(color: _textMuted, fontSize: 13)),
),
DropdownButtonFormField<String>(
value: _signupDistrict,
dropdownColor: const Color(0xFF1A1A1A),
iconEnabledColor: _textMuted,
style: const TextStyle(color: _textWhite, fontSize: 14),
decoration: _glassInputDecoration(
hint: 'Select your district',
prefixIcon: Icons.location_on_outlined,
),
items: _districts
.map((d) => DropdownMenuItem(
value: d,
child: Text(d, style: const TextStyle(color: _textWhite)),
))
.toList(),
onChanged: (v) => setState(() => _signupDistrict = v),
),
const SizedBox(height: 16),
// Password
const Padding(
padding: EdgeInsets.only(left: 4, bottom: 8),
child: Text('Password', style: TextStyle(color: _textMuted, fontSize: 13)),
),
TextFormField(
controller: _signupPassCtrl,
obscureText: _signupObscurePass,
style: const TextStyle(color: _textWhite, fontSize: 14),
cursorColor: Colors.white54,
decoration: _glassInputDecoration(
hint: 'Create a password',
prefixIcon: Icons.lock_outline_rounded,
suffixIcon: IconButton(
icon: Icon(
_signupObscurePass ? Icons.visibility_off_outlined : Icons.visibility_outlined,
color: _textMuted,
size: 20,
),
onPressed: () => setState(() => _signupObscurePass = !_signupObscurePass),
),
),
validator: _passwordValidator,
textInputAction: TextInputAction.next,
),
const SizedBox(height: 16),
// Confirm password
const Padding(
padding: EdgeInsets.only(left: 4, bottom: 8),
child: Text('Confirm password', style: TextStyle(color: _textMuted, fontSize: 13)),
),
TextFormField(
controller: _signupConfirmCtrl,
obscureText: _signupObscureConfirm,
style: const TextStyle(color: _textWhite, fontSize: 14),
cursorColor: Colors.white54,
decoration: _glassInputDecoration(
hint: 'Re-enter your password',
prefixIcon: Icons.lock_outline_rounded,
suffixIcon: IconButton(
icon: Icon(
_signupObscureConfirm ? Icons.visibility_off_outlined : Icons.visibility_outlined,
color: _textMuted,
size: 20,
),
onPressed: () => setState(() => _signupObscureConfirm = !_signupObscureConfirm),
),
),
validator: _passwordValidator,
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _performSignup(),
),
const SizedBox(height: 24),
// Create Account button
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: Container(
onPressed: _loading ? null : _performRegister, decoration: BoxDecoration(
child: _loading ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : const Text('Register'), borderRadius: BorderRadius.circular(28),
gradient: const LinearGradient(
colors: [Color(0xFF2A2A2A), Color(0xFF1A1A1A)],
),
border: Border.all(color: const Color(0x33FFFFFF)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.4),
blurRadius: 16,
offset: const Offset(0, 6),
),
],
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(28),
onTap: _loading ? null : _performSignup,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Center(
child: _loading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2, color: _textWhite),
)
: const Text(
'Create Account',
style: TextStyle(color: _textWhite, fontSize: 16, fontWeight: FontWeight.w600),
),
),
),
),
),
),
),
const SizedBox(height: 24),
// Back to Sign in
Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Already have an account? ',
style: TextStyle(color: _textMuted, fontSize: 13),
),
GestureDetector(
onTap: _openLogin,
child: const Text(
'Sign in',
style: TextStyle(
color: _textWhite,
fontSize: 13,
fontWeight: FontWeight.w600,
decoration: TextDecoration.underline,
decorationColor: _textWhite,
),
), ),
), ),
], ],
), ),
), ),
), ],
),
),
),
),
), ),
); );
} }

View File

@@ -15,7 +15,7 @@ class SettingsScreen extends StatefulWidget {
class _SettingsScreenState extends State<SettingsScreen> { class _SettingsScreenState extends State<SettingsScreen> {
bool _notifications = true; bool _notifications = true;
String _appVersion = '2.0(b)'; String _appVersion = '2.0.4';
int _selectedSection = 0; // 0=Preferences, 1=Account, 2=About int _selectedSection = 0; // 0=Preferences, 1=Account, 2=About
@override @override

View File

@@ -1,7 +1,7 @@
name: figma name: figma
description: A Flutter event app description: A Flutter event app
publish_to: 'none' publish_to: 'none'
version: 2.0.0+20 version: 2.0.4+24
environment: environment:
sdk: ">=2.17.0 <3.0.0" sdk: ">=2.17.0 <3.0.0"