diff --git a/lib/BlindMasterScreens/Startup/create_user_screen.dart b/lib/BlindMasterScreens/Startup/create_user_screen.dart index 7995114..8ad8c2a 100644 --- a/lib/BlindMasterScreens/Startup/create_user_screen.dart +++ b/lib/BlindMasterScreens/Startup/create_user_screen.dart @@ -6,6 +6,8 @@ import 'package:flutter/material.dart'; import 'package:blind_master/BlindMasterResources/text_inputs.dart'; import 'package:blind_master/BlindMasterResources/title_text.dart'; +import 'package:blind_master/BlindMasterScreens/Startup/verification_waiting_screen.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class CreateUserScreen extends StatefulWidget { const CreateUserScreen({super.key}); @@ -44,18 +46,25 @@ class _CreateUserScreenState extends State { if (response.statusCode == 201) { if(mounted) { - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - backgroundColor: Colors.green[800], - content: Text( - "Account Successfully Created!", - textAlign: TextAlign.center, - style: TextStyle(fontSize: 15), + // Extract token from response body + final body = json.decode(response.body); + final token = body['token']; + + if (token != null && token.isNotEmpty) { + // Store token temporarily for verification checking + final storage = FlutterSecureStorage(); + await storage.write(key: 'temp_token', value: token); + + // Navigate to verification waiting screen + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => VerificationWaitingScreen(token: token), ), - ) - ); - Navigator.pop(context); + ); + } else { + throw Exception('No token received from server'); + } } } else { if (response.statusCode == 409) throw Exception('Email Already In Use!'); diff --git a/lib/BlindMasterScreens/Startup/login_screen.dart b/lib/BlindMasterScreens/Startup/login_screen.dart index b0cee5f..22b9a43 100644 --- a/lib/BlindMasterScreens/Startup/login_screen.dart +++ b/lib/BlindMasterScreens/Startup/login_screen.dart @@ -51,6 +51,23 @@ class _LoginScreenState extends State { final response = await regularPost(payload, 'login'); if (response.statusCode != 200) { if (response.statusCode == 400) {throw Exception('Email and Password Necessary');} + else if (response.statusCode == 403) { + // Email not verified + if (!mounted) return; + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: Colors.orange[700], + duration: Duration(seconds: 4), + content: Text( + "Your account has not been verified. Please check your email from blindmasterapp@wahwa.com and verify your account.", + textAlign: TextAlign.center, + style: TextStyle(fontSize: 15), + ), + ) + ); + return; + } else if (response.statusCode == 429) { final body = json.decode(response.body); final retryAfter = body['retryAfter'] ?? 'some time'; diff --git a/lib/BlindMasterScreens/Startup/verification_waiting_screen.dart b/lib/BlindMasterScreens/Startup/verification_waiting_screen.dart new file mode 100644 index 0000000..72d748e --- /dev/null +++ b/lib/BlindMasterScreens/Startup/verification_waiting_screen.dart @@ -0,0 +1,217 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:blind_master/BlindMasterResources/error_snackbar.dart'; +import 'package:flutter/material.dart'; +import 'package:blind_master/BlindMasterResources/title_text.dart'; +import 'package:http/http.dart' as http; + +class VerificationWaitingScreen extends StatefulWidget { + final String token; + + const VerificationWaitingScreen({super.key, required this.token}); + + @override + State createState() => _VerificationWaitingScreenState(); +} + +class _VerificationWaitingScreenState extends State { + Timer? _pollingTimer; + bool _isChecking = false; + + @override + void initState() { + super.initState(); + _startPolling(); + } + + @override + void dispose() { + _pollingTimer?.cancel(); + super.dispose(); + } + + void _startPolling() { + _pollingTimer = Timer.periodic(Duration(seconds: 5), (timer) { + _checkVerificationStatus(); + }); + } + + Future _checkVerificationStatus() async { + if (_isChecking) return; + + setState(() { + _isChecking = true; + }); + + try { + final uri = Uri.parse('https://wahwa.com').replace(path: 'verification_status'); + + final response = await http.get( + uri, + headers: { + 'Authorization': 'Bearer ${widget.token}', + 'Content-Type': 'application/json', + }, + ).timeout(const Duration(seconds: 10)); + + if (response.statusCode == 200) { + final body = json.decode(response.body); + if (body['is_verified'] == true) { + _pollingTimer?.cancel(); + if (!mounted) return; + + Navigator.of(context).popUntil((route) => route.isFirst); + + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: Colors.green[800], + duration: Duration(seconds: 4), + content: Text( + "Account verified successfully! You can now log in.", + textAlign: TextAlign.center, + style: TextStyle(fontSize: 15), + ), + ) + ); + } + } + } catch (e) { + // Silently fail for polling - don't show error to user + print('Verification status check failed: $e'); + } finally { + if (mounted) { + setState(() { + _isChecking = false; + }); + } + } + } + + Future _resendVerificationEmail() async { + try { + final uri = Uri.parse('https://wahwa.com').replace(path: 'resend_verification'); + + final response = await http.post( + uri, + headers: { + 'Authorization': 'Bearer ${widget.token}', + 'Content-Type': 'application/json', + }, + body: json.encode({}), + ).timeout(const Duration(seconds: 10)); + + if (!mounted) return; + + if (response.statusCode == 200) { + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: Colors.green[800], + content: Text( + "Verification email sent! Please check your inbox.", + textAlign: TextAlign.center, + style: TextStyle(fontSize: 15), + ), + ) + ); + } else if (response.statusCode == 429) { + final body = json.decode(response.body); + final retryAfter = body['retryAfter'] ?? 20; + throw Exception('Please wait $retryAfter seconds before requesting another email.'); + } else { + throw Exception('Failed to resend verification email'); + } + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + errorSnackbar(e) + ); + } + } + + Future _onRefresh() async { + await _checkVerificationStatus(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: RefreshIndicator( + onRefresh: _onRefresh, + child: Container( + color: Theme.of(context).primaryColorLight, + child: ListView( + physics: AlwaysScrollableScrollPhysics(), + children: [ + Container( + height: MediaQuery.of(context).size.height, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TitleText("Verify Your Email", txtClr: Colors.white), + SizedBox(height: 30), + Padding( + padding: EdgeInsets.symmetric(horizontal: 40), + child: Text( + "We've sent a verification link to your email from blindmasterapp@wahwa.com", + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ), + SizedBox(height: 20), + if (_isChecking) + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ) + else + Icon( + Icons.email_outlined, + size: 80, + color: Colors.white70, + ), + SizedBox(height: 30), + Padding( + padding: EdgeInsets.symmetric(horizontal: 40), + child: Text( + "Click the link in the email to verify your account. This page will automatically update once verified.", + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white70, + fontSize: 14, + ), + ), + ), + SizedBox(height: 40), + ElevatedButton( + onPressed: _resendVerificationEmail, + child: Text("Resend Verification Email"), + ), + SizedBox(height: 20), + Padding( + padding: EdgeInsets.symmetric(horizontal: 40), + child: Text( + "Pull down to check verification status manually", + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white54, + fontSize: 12, + fontStyle: FontStyle.italic, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/BlindMasterScreens/account_screen.dart b/lib/BlindMasterScreens/account_screen.dart new file mode 100644 index 0000000..8d8e06a --- /dev/null +++ b/lib/BlindMasterScreens/account_screen.dart @@ -0,0 +1,181 @@ +import 'dart:convert'; +import 'package:blind_master/BlindMasterResources/error_snackbar.dart'; +import 'package:blind_master/BlindMasterResources/secure_transmissions.dart'; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class AccountScreen extends StatefulWidget { + const AccountScreen({super.key}); + + @override + State createState() => _AccountScreenState(); +} + +class _AccountScreenState extends State { + String? name; + String? email; + String? createdAt; + bool isLoading = true; + + @override + void initState() { + super.initState(); + fetchAccountInfo(); + } + + Future fetchAccountInfo() async { + try { + final response = await secureGet('account_info'); + + if (response == null) { + throw Exception('No response from server'); + } + + if (response.statusCode == 200) { + final body = json.decode(response.body); + setState(() { + name = body['name'] ?? 'N/A'; + email = body['email'] ?? 'N/A'; + + // Parse and format the created_at timestamp + if (body['created_at'] != null) { + try { + final DateTime dateTime = DateTime.parse(body['created_at']); + final months = ['January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December']; + createdAt = '${months[dateTime.month - 1]} ${dateTime.day}, ${dateTime.year}'; + } catch (e) { + createdAt = 'N/A'; + } + } else { + createdAt = 'N/A'; + } + + isLoading = false; + }); + } else { + throw Exception('Failed to load account info'); + } + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar(errorSnackbar(e)); + setState(() { + isLoading = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).primaryColorLight, + foregroundColor: Colors.white, + title: Text( + 'Account', + style: GoogleFonts.aBeeZee(), + ), + ), + body: isLoading + ? Center( + child: CircularProgressIndicator( + color: Theme.of(context).primaryColorLight, + ), + ) + : SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Account Info Section + Card( + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: CircleAvatar( + radius: 50, + backgroundColor: Theme.of(context).primaryColorLight, + child: Icon( + Icons.person, + size: 60, + color: Colors.white, + ), + ), + ), + SizedBox(height: 20), + _buildInfoRow('Name', name ?? 'N/A'), + Divider(height: 30), + _buildInfoRow('Email', email ?? 'N/A'), + Divider(height: 30), + _buildInfoRow('Member Since', createdAt ?? 'N/A'), + ], + ), + ), + ), + SizedBox(height: 30), + + // Account Options Section + Text( + 'Account Options', + style: GoogleFonts.aBeeZee( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 15), + Card( + elevation: 2, + child: Column( + children: [ + // Placeholder for future options + Padding( + padding: const EdgeInsets.all(20.0), + child: Center( + child: Text( + 'Additional options will appear here', + style: TextStyle( + color: Colors.grey[600], + fontStyle: FontStyle.italic, + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildInfoRow(String label, String value) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 5), + Text( + value, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ], + ); + } +} diff --git a/lib/BlindMasterScreens/home_screen.dart b/lib/BlindMasterScreens/home_screen.dart index aa0f685..4af0361 100644 --- a/lib/BlindMasterScreens/home_screen.dart +++ b/lib/BlindMasterScreens/home_screen.dart @@ -1,6 +1,12 @@ +import 'dart:convert'; import 'package:blind_master/BlindMasterScreens/groupControl/groups_menu.dart'; import 'package:blind_master/BlindMasterScreens/individualControl/devices_menu.dart'; +import 'package:blind_master/BlindMasterScreens/Startup/splash_screen.dart'; +import 'package:blind_master/BlindMasterScreens/account_screen.dart'; +import 'package:blind_master/BlindMasterResources/secure_transmissions.dart'; +import 'package:blind_master/BlindMasterResources/error_snackbar.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:google_fonts/google_fonts.dart'; class HomeScreen extends StatefulWidget { @@ -13,23 +19,102 @@ class HomeScreen extends StatefulWidget { class _HomeScreenState extends State { int currentPageIndex = 0; String greeting = ""; + String? userName; @override void initState() { super.initState(); + fetchUserName(); getGreeting(); } + Future fetchUserName() async { + try { + final response = await secureGet('account_info'); + + if (response != null && response.statusCode == 200) { + final body = json.decode(response.body); + final name = body['name']; + + // Only set userName if it's not null, not empty, and not just whitespace + if (name != null && name.toString().trim().isNotEmpty) { + setState(() { + userName = name.toString().trim(); + getGreeting(); // Update greeting with name + }); + } + } + } catch (e) { + // Silently fail - user will just see generic greeting + } + } + void getGreeting() { final hour = DateTime.now().hour; + String timeGreeting; if (hour >= 5 && hour < 12) { - greeting = "Good Morning!"; + timeGreeting = "Good Morning"; } else if (hour >= 12 && hour < 18) { - greeting = "Good Afternoon!"; + timeGreeting = "Good Afternoon"; } else if (hour >= 18 && hour < 22) { - greeting = "Good Evening!"; - } else {greeting = "😴";} + timeGreeting = "Good Evening"; + } else { + greeting = "😴"; + return; + } + + // Add name if available, otherwise just add exclamation mark + greeting = userName != null ? "$timeGreeting, $userName!" : "$timeGreeting!"; + } + + Future handleLogout() async { + try { + // Show loading indicator + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return Center( + child: CircularProgressIndicator( + color: Theme.of(context).primaryColorLight, + ), + ); + }, + ); + + // Call logout endpoint + final response = await securePost({}, 'logout'); + + // Remove loading indicator + if (mounted) Navigator.of(context).pop(); + + if (response == null || response.statusCode != 200) { + throw Exception('Logout failed'); + } + + // Clear stored token + final storage = FlutterSecureStorage(); + await storage.delete(key: 'token'); + + // Navigate to splash screen (which will redirect to login) + if (!mounted) return; + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => SplashScreen()), + (route) => false, + ); + } catch (e) { + // Remove loading indicator if still showing + if (mounted && Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } + + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + errorSnackbar(e) + ); + } } @override @@ -44,6 +129,58 @@ class _HomeScreenState extends State { ), foregroundColor: Colors.white, ), + drawer: Drawer( + child: Column( + children: [ + DrawerHeader( + decoration: BoxDecoration( + color: Theme.of(context).primaryColorLight, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.blinds, + size: 60, + color: Colors.white, + ), + SizedBox(height: 10), + Text( + 'BlindMaster', + style: GoogleFonts.aBeeZee( + fontSize: 24, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + Spacer(), + Divider(), + ListTile( + leading: Icon(Icons.account_circle), + title: Text('Account'), + onTap: () { + Navigator.pop(context); // Close drawer + Navigator.push( + context, + MaterialPageRoute(builder: (context) => AccountScreen()), + ); + }, + ), + ListTile( + leading: Icon(Icons.logout), + title: Text('Logout'), + onTap: () { + Navigator.pop(context); // Close drawer + handleLogout(); + }, + ), + SizedBox(height: 20), + ], + ), + ), bottomNavigationBar: NavigationBar( onDestinationSelected: (int index) { setState(() { diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..22c90b1 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "blind_master", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}