merge: v2.0.4+24 login fixes + backend URL fix

- Google OAuth serverClientId wired (639347358523-mtkm...apps.googleusercontent.com)
- Timeout 10s→25s + retry on SocketException/TimeoutException
- Forgot Password glassmorphism bottom sheet with safe-degrade
- Same-page signup AnimatedSwitcher (mobile + desktop); delete old RegisterScreen classes
- Guest SnackBar removed from HomeScreen; LoginScreen clearSnackBars() guard
- baseUrl: em.eventifyplus.com → backend.eventifyplus.com (broken TLS fix — real root cause)
- Version: 2.0.4+24

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-19 21:44:10 +05:30
9 changed files with 882 additions and 373 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

@@ -2,12 +2,12 @@
class ApiEndpoints { class ApiEndpoints {
// Change this to your desired backend base URL (local or UAT) // Change this to your desired backend base URL (local or UAT)
// For local Django dev use: "http://127.0.0.1:8000/api" // For local Django dev use: "http://127.0.0.1:8000/api"
// For UAT: "https://uat.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://em.eventifyplus.com/api"; static const String baseUrl = "https://backend.eventifyplus.com/api";
/// Base URL for media files (images, icons uploaded via Django admin). /// Base URL for media files (images, icons uploaded via Django admin).
/// Relative paths like `/media/...` are resolved against this. /// 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 // Auth
static const String register = "$baseUrl/user/register/"; static const String register = "$baseUrl/user/register/";
@@ -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,24 +113,292 @@ 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;
}
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<void> _openForgotPasswordDialog() async {
final emailCtrl = TextEditingController(text: _emailCtrl.text.trim());
bool submitting = false;
await showDialog<void>(
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<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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final screenW = MediaQuery.of(context).size.width; final screenW = MediaQuery.of(context).size.width;
final double safeInitialWidth = (screenW * 0.45).clamp(360.0, screenW * 0.65); final double safeInitialWidth = (screenW * 0.45).clamp(360.0, screenW * 0.65);
final bool animAvailable = _controller != null && _leftWidthAnim != null; final bool animAvailable = _controller != null && _leftWidthAnim != null;
final Listenable animation = _controller ?? AlwaysStoppedAnimation(0.0); final Listenable animation = _controller ?? AlwaysStoppedAnimation(0.0);
@@ -139,7 +418,6 @@ class _DesktopLoginScreenState extends State<DesktopLoginScreen> with SingleTick
Container( Container(
width: leftWidth, width: leftWidth,
height: double.infinity, height: double.infinity,
// color: const Color(0xFF0B63D6),
decoration: AppDecoration.blueGradient, decoration: AppDecoration.blueGradient,
padding: const EdgeInsets.symmetric(horizontal: 36, vertical: 28), padding: const EdgeInsets.symmetric(horizontal: 36, vertical: 28),
child: Opacity( child: Opacity(
@@ -150,11 +428,16 @@ class _DesktopLoginScreenState extends State<DesktopLoginScreen> with SingleTick
const SizedBox(height: 4), const SizedBox(height: 4),
const Text('EVENTIFY', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 22)), const Text('EVENTIFY', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 22)),
const Spacer(), 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 SizedBox(height: 12),
const Text( Text(
'Sign in to access your dashboard, manage events, and stay connected.', _isSignupMode
style: TextStyle(color: Colors.white70, fontSize: 14), ? '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), const Spacer(flex: 2),
Opacity( Opacity(
@@ -168,7 +451,6 @@ class _DesktopLoginScreenState extends State<DesktopLoginScreen> with SingleTick
), ),
), ),
), ),
Expanded( Expanded(
child: Transform.translate( child: Transform.translate(
offset: Offset(formOffset, 0), offset: Offset(formOffset, 0),
@@ -178,85 +460,20 @@ class _DesktopLoginScreenState extends State<DesktopLoginScreen> with SingleTick
color: Colors.white, color: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 36), padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 36),
child: Center( child: Center(
child: ConstrainedBox( child: SingleChildScrollView(
constraints: const BoxConstraints(maxWidth: 520), child: ConstrainedBox(
child: Card( constraints: const BoxConstraints(maxWidth: 520),
elevation: 0, child: Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), elevation: 0,
child: Padding( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
padding: const EdgeInsets.symmetric(horizontal: 28.0, vertical: 28.0), child: Padding(
child: Column( padding: const EdgeInsets.symmetric(horizontal: 28.0, vertical: 28.0),
mainAxisSize: MainAxisSize.min, child: AnimatedSwitcher(
crossAxisAlignment: CrossAxisAlignment.stretch, duration: const Duration(milliseconds: 260),
children: [ child: _isSignupMode
const Text('Sign In', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), textAlign: TextAlign.center), ? _buildSignupFields(safeInitialWidth)
const SizedBox(height: 6), : _buildLoginFields(safeInitialWidth),
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'),
),
],
)
],
), ),
), ),
), ),
@@ -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,6 @@
// 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/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';
@@ -137,12 +136,9 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
_loading = false; _loading = false;
}); });
} }
} catch (e, st) { } catch (e) {
if (kDebugMode) debugPrint('HomeScreen._loadUserDataAndEvents error: $e\n$st'); if (kDebugMode) debugPrint('HomeScreen init unexpected error: $e');
if (mounted) { if (mounted) setState(() => _loading = false);
setState(() => _loading = false);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e))));
}
} }
// Refresh notification badge count (fire-and-forget, skip for guests — endpoint is authed) // Refresh notification badge count (fire-and-forget, skip for guests — endpoint is authed)

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,15 +485,23 @@ 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: Form( child: AnimatedSwitcher(
key: _formKey, duration: const Duration(milliseconds: 280),
child: Column( switchInCurve: Curves.easeOut,
crossAxisAlignment: CrossAxisAlignment.start, switchOutCurve: Curves.easeIn,
children: [ child: _isSignupMode
// Brand name ? KeyedSubtree(key: const ValueKey('signup'), child: _buildSignupForm(context))
Center( : KeyedSubtree(
child: Text( key: const ValueKey('login'),
'Eventify', child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Brand name
Center(
child: Text(
'Eventify',
style: TextStyle( style: TextStyle(
color: _textWhite.withOpacity(0.7), color: _textWhite.withOpacity(0.7),
fontSize: 16, fontSize: 16,
@@ -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),
@@ -568,8 +780,10 @@ class _LoginScreenState extends State<LoginScreen> {
), ),
), ),
), ),
], ],
), ),
),
),
), ),
), ),
), ),
@@ -579,144 +793,233 @@ 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,
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; // District
String? _selectedDistrict; 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),
static const _districts = [ // Password
'Thiruvananthapuram', 'Kollam', 'Pathanamthitta', 'Alappuzha', const Padding(
'Kottayam', 'Idukki', 'Ernakulam', 'Thrissur', 'Palakkad', padding: EdgeInsets.only(left: 4, bottom: 8),
'Malappuram', 'Kozhikode', 'Wayanad', 'Kannur', 'Kasaragod', 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 // Confirm password
void dispose() { const Padding(
_emailCtrl.dispose(); padding: EdgeInsets.only(left: 4, bottom: 8),
_phoneCtrl.dispose(); child: Text('Confirm password', style: TextStyle(color: _textMuted, fontSize: 13)),
_passCtrl.dispose(); ),
_confirmCtrl.dispose(); TextFormField(
super.dispose(); 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<void> _performRegister() async { // Create Account button
if (!(_formKey.currentState?.validate() ?? false)) return; SizedBox(
width: double.infinity,
final email = _emailCtrl.text.trim(); child: Container(
final phone = _phoneCtrl.text.trim(); decoration: BoxDecoration(
final pass = _passCtrl.text; borderRadius: BorderRadius.circular(28),
final confirm = _confirmCtrl.text; gradient: const LinearGradient(
colors: [Color(0xFF2A2A2A), Color(0xFF1A1A1A)],
if (pass != confirm) { ),
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Passwords do not match'))); border: Border.all(color: const Color(0x33FFFFFF)),
return; boxShadow: [
} BoxShadow(
color: Colors.black.withOpacity(0.4),
setState(() => _loading = true); blurRadius: 16,
offset: const Offset(0, 6),
try { ),
await _auth.register( ],
email: email, ),
phoneNumber: phone, child: Material(
password: pass, color: Colors.transparent,
district: _selectedDistrict, child: InkWell(
); borderRadius: BorderRadius.circular(28),
onTap: _loading ? null : _performSignup,
if (!mounted) return; child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const HomeScreen())); child: Center(
} catch (e) { child: _loading
if (!mounted) return; ? const SizedBox(
final message = userFriendlyError(e); height: 20,
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); width: 20,
} finally { child: CircularProgressIndicator(strokeWidth: 2, color: _textWhite),
if (mounted) setState(() => _loading = false); )
} : const Text(
} 'Create Account',
style: TextStyle(color: _textWhite, fontSize: 16, fontWeight: FontWeight.w600),
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<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),
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'),
),
),
],
), ),
), ),
), ),
), ),
), ),
), ),
), 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.3'; 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.3+23 version: 2.0.4+24
environment: environment:
sdk: ">=2.17.0 <3.0.0" sdk: ">=2.17.0 <3.0.0"