From ebe654f9c3098675ccc87a21ce0a367e8812759e Mon Sep 17 00:00:00 2001 From: Sicherhaven Date: Sun, 19 Apr 2026 21:40:17 +0530 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20v2.0.4+24=20=E2=80=94=20login=20fixe?= =?UTF-8?q?s,=20signup=20toggle,=20forgot-password,=20guest=20SnackBar,=20?= =?UTF-8?q?Google=20OAuth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- android/app/proguard-rules.pro | 59 ++ lib/core/api/api_client.dart | 39 +- lib/core/api/api_endpoints.dart | 1 + lib/features/auth/services/auth_service.dart | 22 +- lib/screens/desktop_login_screen.dart | 533 ++++++++++------- lib/screens/home_screen.dart | 8 +- lib/screens/login_screen.dart | 581 ++++++++++++++----- lib/screens/settings_screen.dart | 2 +- pubspec.yaml | 2 +- 9 files changed, 879 insertions(+), 368 deletions(-) diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro index e90819d..739d28a 100644 --- a/android/app/proguard-rules.pro +++ b/android/app/proguard-rules.pro @@ -26,3 +26,62 @@ -dontwarn com.google.android.play.core.tasks.OnFailureListener -dontwarn com.google.android.play.core.tasks.OnSuccessListener -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 * { + ; + ; +} + +# 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 ; +} + +# 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 ; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} diff --git a/lib/core/api/api_client.dart b/lib/core/api/api_client.dart index d7ae792..9a39ee4 100644 --- a/lib/core/api/api_client.dart +++ b/lib/core/api/api_client.dart @@ -1,12 +1,15 @@ // lib/core/api/api_client.dart +import 'dart:async'; import 'dart:convert'; +import 'dart:io' show SocketException; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:http_parser/http_parser.dart'; import '../storage/token_storage.dart'; 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) static const bool _developmentMode = false; @@ -28,13 +31,7 @@ class ApiClient { late http.Response response; try { - response = await http - .post( - Uri.parse(url), - headers: headers, - body: jsonEncode(finalBody), - ) - .timeout(_timeout); + response = await _postWithRetry(url, headers, finalBody); } catch (e) { if (kDebugMode) debugPrint('ApiClient.post network error: $e'); @@ -100,6 +97,32 @@ class ApiClient { return _handleResponse(url, response, finalBody); } + /// POST with one retry on transient network errors. + /// Retries on SocketException / TimeoutException only. + Future _postWithRetry( + String url, + Map headers, + Map 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. /// /// Returns the `file` object from the server response: diff --git a/lib/core/api/api_endpoints.dart b/lib/core/api/api_endpoints.dart index 8a89eda..480583b 100644 --- a/lib/core/api/api_endpoints.dart +++ b/lib/core/api/api_endpoints.dart @@ -15,6 +15,7 @@ class ApiEndpoints { static const String logout = "$baseUrl/user/logout/"; static const String status = "$baseUrl/user/status/"; static const String updateProfile = "$baseUrl/user/update-profile/"; + static const String forgotPassword = "$baseUrl/user/forgot-password/"; // Events static const String eventTypes = "$baseUrl/events/type-list/"; // list of event types diff --git a/lib/features/auth/services/auth_service.dart b/lib/features/auth/services/auth_service.dart index 81b01e0..145eabd 100644 --- a/lib/features/auth/services/auth_service.dart +++ b/lib/features/auth/services/auth_service.dart @@ -12,6 +12,13 @@ import '../models/user_model.dart'; class AuthService { 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 Future login(String username, String password) async { try { @@ -158,7 +165,10 @@ class AuthService { /// GOOGLE OAUTH LOGIN → returns UserModel Future googleLogin() async { try { - final googleSignIn = GoogleSignIn(scopes: ['email']); + final googleSignIn = GoogleSignIn( + scopes: const ['email', 'profile'], + serverClientId: _googleWebClientId, + ); final account = await googleSignIn.signIn(); 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 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) Future logout() async { try { diff --git a/lib/screens/desktop_login_screen.dart b/lib/screens/desktop_login_screen.dart index 803f6a6..906a19f 100644 --- a/lib/screens/desktop_login_screen.dart +++ b/lib/screens/desktop_login_screen.dart @@ -15,9 +15,23 @@ class DesktopLoginScreen extends StatefulWidget { } class _DesktopLoginScreenState extends State with SingleTickerProviderStateMixin { + // Login controllers final TextEditingController _emailCtrl = 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(); AnimationController? _controller; @@ -31,13 +45,18 @@ class _DesktopLoginScreenState extends State with SingleTick final Curve _curve = Curves.easeInOutCubic; bool _isAnimating = false; - bool _loading = false; // network loading flag + bool _loading = false; + bool _isSignupMode = false; @override void dispose() { _controller?.dispose(); _emailCtrl.dispose(); _passCtrl.dispose(); + _signupEmailCtrl.dispose(); + _signupPhoneCtrl.dispose(); + _signupPassCtrl.dispose(); + _signupConfirmCtrl.dispose(); super.dispose(); } @@ -52,7 +71,6 @@ class _DesktopLoginScreenState extends State with SingleTick _leftTextOpacityAnim = Tween(begin: 1.0, end: 0.0).animate( CurvedAnimation(parent: _controller!, curve: const Interval(0.0, 0.35, curve: Curves.easeOut)), ); - _formOpacityAnim = Tween(begin: 1.0, end: 0.0).animate( CurvedAnimation(parent: _controller!, curve: const Interval(0.0, 0.55, curve: Curves.easeOut)), ); @@ -68,9 +86,7 @@ class _DesktopLoginScreenState extends State with SingleTick Future _performLoginFlow(double initialLeftWidth) async { if (_isAnimating || _loading) return; - setState(() { - _loading = true; - }); + setState(() => _loading = true); final email = _emailCtrl.text.trim(); final password = _passCtrl.text; @@ -87,14 +103,9 @@ class _DesktopLoginScreenState extends State with SingleTick } try { - // Capture user model returned by AuthService (AuthService already saves prefs) await _auth.login(email, password); - - // on success run opening animation await _startCollapseAnimation(initialLeftWidth); - if (!mounted) return; - Navigator.of(context).pushReplacement(PageRouteBuilder( pageBuilder: (context, a1, a2) => const HomeDesktopScreen(skipSidebarEntranceAnimation: true), transitionDuration: Duration.zero, @@ -102,24 +113,292 @@ class _DesktopLoginScreenState extends State with SingleTick )); } catch (e) { if (!mounted) return; - final message = userFriendlyError(e); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e)))); setState(() => _isAnimating = false); } finally { - if (mounted) setState(() { - _loading = false; - }); + if (mounted) setState(() => _loading = false); } } - void _openRegister() { - Navigator.of(context).push(MaterialPageRoute(builder: (_) => const DesktopRegisterScreen())); + Future _performSignupFlow(double initialLeftWidth) async { + 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; + } + + setState(() => _loading = true); + + try { + await _auth.register( + 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); + } + } + + Future _openForgotPasswordDialog() async { + final emailCtrl = TextEditingController(text: _emailCtrl.text.trim()); + bool submitting = false; + + await showDialog( + context: context, + builder: (ctx) { + return StatefulBuilder( + builder: (ctx, setDialog) { + return AlertDialog( + title: const Text('Forgot Password'), + content: SizedBox( + width: 360, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Enter your email and we'll send reset instructions."), + const SizedBox(height: 12), + TextField( + controller: emailCtrl, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.email), + labelText: 'Email', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + ), + 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'), + ), + ], + ); + }, + ); + }, + ); + + emailCtrl.dispose(); + } + + Widget _buildLoginFields(double safeInitialWidth) { + return Column( + key: const ValueKey('login'), + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Text('Sign In', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), textAlign: TextAlign.center), + const SizedBox(height: 6), + const Text('Please enter your details to continue', textAlign: TextAlign.center, style: TextStyle(color: Colors.black54)), + const SizedBox(height: 22), + TextField( + controller: _emailCtrl, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.email), + labelText: 'Email Address', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + ), + ), + const SizedBox(height: 12), + TextField( + controller: _passCtrl, + obscureText: true, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.lock), + labelText: 'Password', + border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), + ), + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row(children: [ + Checkbox(value: true, onChanged: (_) {}), + const Text('Remember me'), + ]), + TextButton(onPressed: _openForgotPasswordDialog, child: const Text('Forgot Password?')), + ], + ), + const SizedBox(height: 8), + SizedBox( + height: 50, + child: ElevatedButton( + onPressed: (_isAnimating || _loading) ? null : () => _performLoginFlow(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('Sign In', style: TextStyle(fontSize: 16)), + ), + ), + const SizedBox(height: 12), + Wrap( + alignment: WrapAlignment.spaceBetween, + children: [ + TextButton( + onPressed: () => setState(() => _isSignupMode = true), + child: const Text("Don't have an account? Register"), + ), + TextButton(onPressed: () {}, child: const Text('Contact support')), + TextButton( + onPressed: () { + AuthGuard.setGuest(true); + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute(builder: (_) => const HomeDesktopScreen(skipSidebarEntranceAnimation: true)), + (route) => false, + ); + }, + 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( + 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); @@ -139,7 +418,6 @@ class _DesktopLoginScreenState extends State with SingleTick Container( width: leftWidth, height: double.infinity, - // color: const Color(0xFF0B63D6), decoration: AppDecoration.blueGradient, padding: const EdgeInsets.symmetric(horizontal: 36, vertical: 28), child: Opacity( @@ -150,11 +428,16 @@ class _DesktopLoginScreenState extends State with SingleTick const SizedBox(height: 4), 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)), + Text( + _isSignupMode ? 'Join Eventify!' : 'Welcome Back!', + style: const TextStyle(color: Colors.white, fontSize: 34, fontWeight: FontWeight.bold), + ), const SizedBox(height: 12), - const Text( - 'Sign in to access your dashboard, manage events, and stay connected.', - style: TextStyle(color: Colors.white70, fontSize: 14), + 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( @@ -168,7 +451,6 @@ class _DesktopLoginScreenState extends State with SingleTick ), ), ), - Expanded( child: Transform.translate( offset: Offset(formOffset, 0), @@ -178,85 +460,20 @@ class _DesktopLoginScreenState extends State with SingleTick 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, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text('Sign In', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), textAlign: TextAlign.center), - const SizedBox(height: 6), - const Text('Please enter your details to continue', textAlign: TextAlign.center, style: TextStyle(color: Colors.black54)), - const SizedBox(height: 22), - TextField( - controller: _emailCtrl, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.email), - labelText: 'Email Address', - border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), - ), - ), - const SizedBox(height: 12), - TextField( - controller: _passCtrl, - obscureText: true, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.lock), - labelText: 'Password', - border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), - ), - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row(children: [ - Checkbox(value: true, onChanged: (_) {}), - const Text('Remember me') - ]), - TextButton(onPressed: () {}, child: const Text('Forgot Password?')) - ], - ), - const SizedBox(height: 8), - SizedBox( - height: 50, - child: ElevatedButton( - onPressed: (_isAnimating || _loading) - ? null - : () { - final double initial = safeInitialWidth; - _performLoginFlow(initial); - }, - 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('Sign In', style: TextStyle(fontSize: 16)), - ), - ), - const SizedBox(height: 12), - Wrap( - alignment: WrapAlignment.spaceBetween, - children: [ - TextButton(onPressed: _openRegister, child: const Text("Don't have an account? Register")), - TextButton(onPressed: () {}, child: const Text('Contact support')), - TextButton( - onPressed: () { - AuthGuard.setGuest(true); - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute(builder: (_) => const HomeDesktopScreen(skipSidebarEntranceAnimation: true)), - (route) => false, - ); - }, - child: const Text('Continue as Guest'), - ), - ], - ) - ], + 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 with SingleTick ); } } - -class DesktopRegisterScreen extends StatefulWidget { - const DesktopRegisterScreen({Key? key}) : super(key: key); - - @override - State createState() => _DesktopRegisterScreenState(); -} - -class _DesktopRegisterScreenState extends State { - 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 _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')), - ], - ) - ], - ), - ), - ), - ), - ), - ), - ), - ); - } -} diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index b65fb6f..194b023 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -1,7 +1,7 @@ // lib/screens/home_screen.dart import 'dart:async'; import 'dart:ui'; -import '../core/utils/error_utils.dart'; +import 'package:flutter/foundation.dart' show kDebugMode, debugPrint; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../core/auth/auth_guard.dart'; @@ -137,10 +137,8 @@ class _HomeScreenState extends State with SingleTickerProviderStateM }); } } catch (e) { - if (mounted) { - setState(() => _loading = false); - ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e)))); - } + if (kDebugMode) debugPrint('HomeScreen init unexpected error: $e'); + if (mounted) setState(() => _loading = false); } // Refresh notification badge count (fire-and-forget) diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index a0af62e..3294748 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -2,7 +2,6 @@ import 'dart:ui'; import '../core/utils/error_utils.dart'; -import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; import 'package:provider/provider.dart'; @@ -22,12 +21,30 @@ class LoginScreen extends StatefulWidget { class _LoginScreenState extends State { final _formKey = GlobalKey(); + final _signupFormKey = GlobalKey(); final TextEditingController _emailCtrl = TextEditingController(); final TextEditingController _passCtrl = TextEditingController(); final FocusNode _emailFocus = 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(); bool _loading = false; bool _obscurePassword = true; @@ -50,6 +67,9 @@ class _LoginScreenState extends State { void initState() { super.initState(); _initVideo(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) ScaffoldMessenger.of(context).clearSnackBars(); + }); } Future _initVideo() async { @@ -72,6 +92,10 @@ class _LoginScreenState extends State { _passCtrl.dispose(); _emailFocus.dispose(); _passFocus.dispose(); + _signupEmailCtrl.dispose(); + _signupPhoneCtrl.dispose(); + _signupPassCtrl.dispose(); + _signupConfirmCtrl.dispose(); super.dispose(); } @@ -123,7 +147,11 @@ class _LoginScreenState extends State { } void _openRegister() { - Navigator.of(context).push(MaterialPageRoute(builder: (_) => const RegisterScreen(isDesktop: false))); + setState(() => _isSignupMode = true); + } + + void _openLogin() { + setState(() => _isSignupMode = false); } void _showComingSoon() { @@ -132,6 +160,182 @@ class _LoginScreenState extends State { ); } + Future _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 _openForgotPasswordSheet() async { + final emailCtrl = TextEditingController(text: _emailCtrl.text.trim()); + final sheetFormKey = GlobalKey(); + bool submitting = false; + + await showModalBottomSheet( + 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 _performGoogleLogin() async { try { setState(() => _loading = true); @@ -281,15 +485,23 @@ class _LoginScreenState extends State { padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 400), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Brand name - Center( - child: Text( - 'Eventify', + 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( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Brand name + Center( + child: Text( + 'Eventify', style: TextStyle( color: _textWhite.withOpacity(0.7), fontSize: 16, @@ -410,7 +622,7 @@ class _LoginScreenState extends State { ), // Forgot Password GestureDetector( - onTap: _showComingSoon, + onTap: _openForgotPasswordSheet, child: const Text( 'Forgot Password?', style: TextStyle(color: _textMuted, fontSize: 12), @@ -568,8 +780,10 @@ class _LoginScreenState extends State { ), ), ), - ], - ), + ], + ), + ), + ), ), ), ), @@ -579,144 +793,233 @@ class _LoginScreenState extends State { ), ); } -} -/// Register screen calls backend register endpoint via AuthService.register -class RegisterScreen extends StatefulWidget { - final bool isDesktop; - const RegisterScreen({Key? key, this.isDesktop = false}) : super(key: key); + Widget _buildSignupForm(BuildContext context) { + return Form( + key: _signupFormKey, + 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 - State createState() => _RegisterScreenState(); -} + // Email + 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 { - final _formKey = GlobalKey(); - final TextEditingController _emailCtrl = TextEditingController(); - final TextEditingController _phoneCtrl = TextEditingController(); - final TextEditingController _passCtrl = TextEditingController(); - final TextEditingController _confirmCtrl = TextEditingController(); - final AuthService _auth = AuthService(); + // Phone + const Padding( + padding: EdgeInsets.only(left: 4, bottom: 8), + child: Text('Phone', style: TextStyle(color: _textMuted, fontSize: 13)), + ), + TextFormField( + controller: _signupPhoneCtrl, + keyboardType: TextInputType.phone, + style: const TextStyle(color: _textWhite, fontSize: 14), + cursorColor: Colors.white54, + decoration: _glassInputDecoration( + hint: 'Enter your phone number', + prefixIcon: Icons.phone_outlined, + ), + validator: (v) { + if (v == null || v.trim().isEmpty) return 'Enter phone number'; + if (v.trim().length < 7) return 'Enter a valid phone number'; + return null; + }, + textInputAction: TextInputAction.next, + ), + const SizedBox(height: 16), - bool _loading = false; - String? _selectedDistrict; + // District + const Padding( + padding: EdgeInsets.only(left: 4, bottom: 8), + child: Text('District (optional)', style: TextStyle(color: _textMuted, fontSize: 13)), + ), + DropdownButtonFormField( + 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), - static const _districts = [ - 'Thiruvananthapuram', 'Kollam', 'Pathanamthitta', 'Alappuzha', - 'Kottayam', 'Idukki', 'Ernakulam', 'Thrissur', 'Palakkad', - 'Malappuram', 'Kozhikode', 'Wayanad', 'Kannur', 'Kasaragod', - ]; + // 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), - @override - void dispose() { - _emailCtrl.dispose(); - _phoneCtrl.dispose(); - _passCtrl.dispose(); - _confirmCtrl.dispose(); - super.dispose(); - } + // 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), - Future _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.trim().length < 7) return 'Enter a valid phone number'; - return null; - } - - 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( - 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), - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: _loading ? null : _performRegister, - child: _loading ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2)) : const Text('Register'), - ), - ), - ], + // Create Account button + 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)), + 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, + ), + ), + ), + ], + ), + ), + ], ), ); } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index cf6a0b7..6c58677 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -15,7 +15,7 @@ class SettingsScreen extends StatefulWidget { class _SettingsScreenState extends State { bool _notifications = true; - String _appVersion = '2.0(b)'; + String _appVersion = '2.0.4'; int _selectedSection = 0; // 0=Preferences, 1=Account, 2=About @override diff --git a/pubspec.yaml b/pubspec.yaml index 005b32c..dbaaae6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: figma description: A Flutter event app publish_to: 'none' -version: 2.0.0+20 +version: 2.0.4+24 environment: sdk: ">=2.17.0 <3.0.0" From b9efe1866961ac413528e166e54da35880c0d29d Mon Sep 17 00:00:00 2001 From: Sicherhaven Date: Sun, 19 Apr 2026 21:42:39 +0530 Subject: [PATCH 2/2] fix: switch baseUrl to backend.eventifyplus.com (broken TLS on em.eventifyplus.com) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit em.eventifyplus.com / uat.eventifyplus.com DNS points to K8s with broken TLS cert. backend.eventifyplus.com → EC2 174.129.72.160 with valid Let's Encrypt cert. This fixes the root cause of "Unable to connect" on all API calls. Co-Authored-By: Claude Sonnet 4.6 --- lib/core/api/api_endpoints.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/core/api/api_endpoints.dart b/lib/core/api/api_endpoints.dart index 480583b..ac30036 100644 --- a/lib/core/api/api_endpoints.dart +++ b/lib/core/api/api_endpoints.dart @@ -2,12 +2,12 @@ class ApiEndpoints { // Change this to your desired backend base URL (local or UAT) // For local Django dev use: "http://127.0.0.1:8000/api" - // For UAT: "https://uat.eventifyplus.com/api" - static const String baseUrl = "https://em.eventifyplus.com/api"; + // em.eventifyplus.com DNS points to K8s with broken TLS — use backend.eventifyplus.com (EC2, valid cert) + static const String baseUrl = "https://backend.eventifyplus.com/api"; /// Base URL for media files (images, icons uploaded via Django admin). /// Relative paths like `/media/...` are resolved against this. - static const String mediaBaseUrl = "https://em.eventifyplus.com"; + static const String mediaBaseUrl = "https://backend.eventifyplus.com"; // Auth static const String register = "$baseUrl/user/register/";