Compare commits

...

5 Commits

Author SHA1 Message Date
3484fa9885 fix(android): add SplashActivity with video intro — fix NPE by moving insetsController after setContentView
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 11:10:50 +05:30
7867e6c728 chore: restore AndroidManifest/Info.plist changes + resolve stash conflicts
Merges in-progress manifest/plist changes (stashed before merge).
Resolves trivial comment conflicts in api_endpoints.dart and auth_service.dart —
both retained backend.eventifyplus.com URL and Google OAuth serverClientId.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 21:44:57 +05:30
98a5d541aa 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>
2026-04-19 21:44:10 +05:30
b9efe18669 fix: switch baseUrl to backend.eventifyplus.com (broken TLS on em.eventifyplus.com)
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 <noreply@anthropic.com>
2026-04-19 21:42:39 +05:30
ebe654f9c3 fix: v2.0.4+24 — login fixes, signup toggle, forgot-password, guest SnackBar, Google OAuth
- Google Sign-In: wire serverClientId (639347358523-mtkm...apps.googleusercontent.com) so idToken is returned on Android
- Email login: raise timeout 10s→25s, add single retry on SocketException/TimeoutException
- Forgot Password: real glassmorphism bottom sheet with safe-degrade SnackBar (endpoint missing on backend)
- Create Account: same-page AnimatedSwitcher toggle with glassmorphism signup form; delete old RegisterScreen
- Desktop parity: DesktopLoginScreen same-page toggle; delete DesktopRegisterScreen
- Guest mode: remove ScaffoldMessenger SnackBar from HomeScreen outer catch (inner _safe wrappers already return [])
- LoginScreen: clearSnackBars() on postFrameCallback to prevent carried-over SnackBars from prior screens
- ProGuard: add Google Sign-In + OkHttp keep rules
- Version bump: 2.0.0+20 → 2.0.4+24; settings _appVersion → 2.0.4

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 21:40:17 +05:30
13 changed files with 987 additions and 378 deletions

View File

@@ -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 * {
<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

@@ -18,6 +18,18 @@
android:name="com.google.android.geo.API_KEY"
android:value="YOUR_GOOGLE_MAPS_API_KEY"/>
<!-- Splash video plays first, then launches MainActivity -->
<activity
android:name=".SplashActivity"
android:exported="true"
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:exported="true"
@@ -31,11 +43,6 @@
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below. Used by Flutter tool. -->

View File

@@ -0,0 +1,80 @@
package com.sicherhaven.eventify
import android.app.Activity
import android.content.Intent
import android.graphics.Color
import android.media.MediaPlayer
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.VideoView
class SplashActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
// White background matches splash logo/video content
val container = FrameLayout(this)
container.setBackgroundColor(Color.WHITE)
setContentView(container, ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
))
// Edge-to-edge: hide both status bar and navigation bar (after setContentView so DecorView exists)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.insetsController?.let {
it.hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
it.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
} else {
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
)
}
val videoView = VideoView(this)
container.addView(videoView, FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
))
val uri = Uri.parse("android.resource://$packageName/${R.raw.splash_video}")
videoView.setVideoURI(uri)
videoView.setOnPreparedListener { mp ->
mp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING)
mp.start()
}
videoView.setOnCompletionListener {
startActivity(Intent(this, MainActivity::class.java))
finish()
}
videoView.setOnErrorListener { _, _, _ ->
startActivity(Intent(this, MainActivity::class.java))
finish()
true
}
videoView.requestFocus()
}
}

Binary file not shown.

View File

@@ -47,5 +47,18 @@
<true/>
<key>UIStatusBarHidden</key>
<false/>
<key>GIDClientID</key>
<string>639347358523-1siq78p4vntem3dtr067ajdhqsv9a2jd.apps.googleusercontent.com</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>com.googleusercontent.apps.639347358523-1siq78p4vntem3dtr067ajdhqsv9a2jd</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@@ -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<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.
///
/// Returns the `file` object from the server response:

View File

@@ -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 / uat.eventifyplus.com DNS → K8s, broken TLS. backend.eventifyplus.com → EC2 174.129.72.160, 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/";
@@ -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

View File

@@ -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<UserModel> login(String username, String password) async {
try {
@@ -158,7 +165,10 @@ class AuthService {
/// GOOGLE OAUTH LOGIN → returns UserModel
Future<UserModel> 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<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)
Future<void> logout() async {
try {

View File

@@ -15,9 +15,23 @@ class DesktopLoginScreen extends StatefulWidget {
}
class _DesktopLoginScreenState extends State<DesktopLoginScreen> 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<DesktopLoginScreen> 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<DesktopLoginScreen> with SingleTick
_leftTextOpacityAnim = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(parent: _controller!, curve: const Interval(0.0, 0.35, curve: Curves.easeOut)),
);
_formOpacityAnim = Tween<double>(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<DesktopLoginScreen> with SingleTick
Future<void> _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<DesktopLoginScreen> 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<DesktopLoginScreen> 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<void> _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<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
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<DesktopLoginScreen> 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<DesktopLoginScreen> 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<DesktopLoginScreen> with SingleTick
),
),
),
Expanded(
child: Transform.translate(
offset: Offset(formOffset, 0),
@@ -178,85 +460,20 @@ class _DesktopLoginScreenState extends State<DesktopLoginScreen> 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<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
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';
@@ -137,12 +136,9 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
_loading = false;
});
}
} catch (e, st) {
if (kDebugMode) debugPrint('HomeScreen._loadUserDataAndEvents error: $e\n$st');
if (mounted) {
setState(() => _loading = false);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(userFriendlyError(e))));
}
} catch (e) {
if (kDebugMode) debugPrint('HomeScreen init unexpected error: $e');
if (mounted) setState(() => _loading = false);
}
// Refresh notification badge count (fire-and-forget, skip for guests — endpoint is authed)

View File

@@ -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<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _signupFormKey = GlobalKey<FormState>();
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<LoginScreen> {
void initState() {
super.initState();
_initVideo();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) ScaffoldMessenger.of(context).clearSnackBars();
});
}
Future<void> _initVideo() async {
@@ -72,6 +92,10 @@ class _LoginScreenState extends State<LoginScreen> {
_passCtrl.dispose();
_emailFocus.dispose();
_passFocus.dispose();
_signupEmailCtrl.dispose();
_signupPhoneCtrl.dispose();
_signupPassCtrl.dispose();
_signupConfirmCtrl.dispose();
super.dispose();
}
@@ -123,7 +147,11 @@ class _LoginScreenState extends State<LoginScreen> {
}
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<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 {
try {
setState(() => _loading = true);
@@ -281,15 +485,23 @@ class _LoginScreenState extends State<LoginScreen> {
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<LoginScreen> {
),
// 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<LoginScreen> {
),
),
),
],
),
],
),
),
),
),
),
),
@@ -579,144 +793,233 @@ class _LoginScreenState extends State<LoginScreen> {
),
);
}
}
/// 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<RegisterScreen> 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<RegisterScreen> {
final _formKey = GlobalKey<FormState>();
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<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 = [
'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<void> _performRegister() async {
if (!(_formKey.currentState?.validate() ?? false)) return;
final email = _emailCtrl.text.trim();
final phone = _phoneCtrl.text.trim();
final pass = _passCtrl.text;
final confirm = _confirmCtrl.text;
if (pass != confirm) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Passwords do not match')));
return;
}
setState(() => _loading = true);
try {
await _auth.register(
email: email,
phoneNumber: phone,
password: pass,
district: _selectedDistrict,
);
if (!mounted) return;
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => const HomeScreen()));
} catch (e) {
if (!mounted) return;
final message = userFriendlyError(e);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
} finally {
if (mounted) setState(() => _loading = false);
}
}
String? _emailValidator(String? v) {
if (v == null || v.trim().isEmpty) return 'Enter email';
final emailRegex = RegExp(r"^[^@]+@[^@]+\.[^@]+");
if (!emailRegex.hasMatch(v.trim())) return 'Enter a valid email';
return null;
}
String? _phoneValidator(String? v) {
if (v == null || v.trim().isEmpty) return 'Enter phone number';
if (v.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'),
),
),
],
// 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,
),
),
),
],
),
),
],
),
);
}

View File

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

View File

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