diff --git a/lib/BlindMasterResources/secure_transmissions.dart b/lib/BlindMasterResources/secure_transmissions.dart index 684bd09..7ca8959 100644 --- a/lib/BlindMasterResources/secure_transmissions.dart +++ b/lib/BlindMasterResources/secure_transmissions.dart @@ -90,6 +90,40 @@ Future securePost(Map payload, String path) asy return response; } +Future secureDelete(String path) async{ + final storage = FlutterSecureStorage(); + final token = await storage.read(key: 'token'); + if (token == null) return null; + + final uri = Uri.parse(socketString).replace( + path: path, + ); + + var response = await http.delete( + uri, + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ) + .timeout(const Duration(seconds: 10)); + + // Retry once if rate limited + if (response.statusCode == 429) { + await Future.delayed(const Duration(seconds: 1)); + response = await http.delete( + uri, + headers: { + 'Authorization': 'Bearer $token', + 'Content-Type': 'application/json', + }, + ) + .timeout(const Duration(seconds: 10)); + } + + return response; +} + Future regularGet(String path) async { final uri = Uri.parse(socketString).replace( path: path, diff --git a/lib/BlindMasterScreens/Startup/create_user_screen.dart b/lib/BlindMasterScreens/Startup/create_user_screen.dart index 71ef59c..71e7f89 100644 --- a/lib/BlindMasterScreens/Startup/create_user_screen.dart +++ b/lib/BlindMasterScreens/Startup/create_user_screen.dart @@ -6,7 +6,7 @@ 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:blind_master/BlindMasterScreens/Startup/verify_new_email_screen.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class CreateUserScreen extends StatefulWidget { diff --git a/lib/BlindMasterScreens/Startup/email_change_waiting_screen.dart b/lib/BlindMasterScreens/Startup/email_change_waiting_screen.dart new file mode 100644 index 0000000..3978940 --- /dev/null +++ b/lib/BlindMasterScreens/Startup/email_change_waiting_screen.dart @@ -0,0 +1,73 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:blind_master/BlindMasterResources/secure_transmissions.dart'; +import 'package:flutter/material.dart'; +import '../email_verification_waiting_screen.dart'; + +class EmailChangeWaitingScreen extends BaseVerificationWaitingScreen { + final String newEmail; + + const EmailChangeWaitingScreen({super.key, required this.newEmail}); + + @override + State createState() => _EmailChangeWaitingScreenState(); +} + +class _EmailChangeWaitingScreenState extends BaseVerificationWaitingScreenState { + @override + String get title => "Verify New Email"; + + @override + String get mainMessage => "We've sent a verification link to:"; + + @override + String? get highlightedInfo => widget.newEmail; + + @override + String get instructionMessage => "Click the link in the email to verify your new email address. This page will automatically update once verified."; + + @override + String get successMessage => "Email changed successfully!"; + + @override + Future checkStatus() async { + final response = await secureGet('pending-email-status'); + + if (response == null) { + throw Exception('No response from server'); + } + + if (response.statusCode == 200) { + final body = json.decode(response.body); + return body['hasPending'] == false; + } + return false; + } + + @override + Future resendVerification() async { + final localHour = DateTime.now().hour; + final response = await securePost( + { + 'newEmail': widget.newEmail, + 'localHour': localHour, + }, + 'request-email-change', + ); + + if (response == null) { + throw Exception('No response from server'); + } + + if (response.statusCode != 200) { + final body = json.decode(response.body); + throw Exception(body['error'] ?? 'Failed to resend verification email'); + } + } + + @override + void onSuccess() { + Navigator.of(context).pop(true); // Return true to indicate success + } +} diff --git a/lib/BlindMasterScreens/Startup/verify_new_email_screen.dart b/lib/BlindMasterScreens/Startup/verify_new_email_screen.dart new file mode 100644 index 0000000..5f07831 --- /dev/null +++ b/lib/BlindMasterScreens/Startup/verify_new_email_screen.dart @@ -0,0 +1,76 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import '../email_verification_waiting_screen.dart'; + +class VerificationWaitingScreen extends BaseVerificationWaitingScreen { + final String token; + + const VerificationWaitingScreen({super.key, required this.token}); + + @override + State createState() => _VerificationWaitingScreenState(); +} + +class _VerificationWaitingScreenState extends BaseVerificationWaitingScreenState { + @override + String get title => "Verify Your Email"; + + @override + String get mainMessage => "We've sent a verification link to your email from blindmasterapp@wahwa.com"; + + @override + String get instructionMessage => "Click the link in the email to verify your account. This page will automatically update once verified."; + + @override + String get successMessage => "Account verified successfully! You can now log in."; + + @override + Future checkStatus() async { + 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); + return body['is_verified'] == true; + } + return false; + } + + @override + Future resendVerification() async { + final localHour = DateTime.now().hour; + 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({'localHour': localHour}), + ).timeout(const Duration(seconds: 10)); + + 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 if (response.statusCode != 200) { + throw Exception('Failed to resend verification email'); + } + } + + @override + void onSuccess() { + Navigator.of(context).popUntil((route) => route.isFirst); + } +} diff --git a/lib/BlindMasterScreens/accountManagement/account_screen.dart b/lib/BlindMasterScreens/accountManagement/account_screen.dart new file mode 100644 index 0000000..ae8905f --- /dev/null +++ b/lib/BlindMasterScreens/accountManagement/account_screen.dart @@ -0,0 +1,431 @@ +import 'dart:convert'; +import 'package:blind_master/BlindMasterResources/error_snackbar.dart'; +import 'package:blind_master/BlindMasterResources/secure_transmissions.dart'; +import 'package:blind_master/BlindMasterScreens/accountManagement/change_password_screen.dart'; +import 'package:blind_master/BlindMasterScreens/accountManagement/verify_email_change_screen.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; + }); + } + } + + Future _handleDeleteAccount() async { + final primaryColor = Theme.of(context).primaryColorLight; + + final confirmed = await showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: Row( + children: [ + Icon(Icons.warning, color: Colors.red), + SizedBox(width: 10), + Text('Delete Account'), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Are you sure you want to delete your account?', + style: TextStyle(fontWeight: FontWeight.bold), + ), + SizedBox(height: 10), + Text('This action cannot be undone. All your data will be permanently deleted.'), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(false), + child: Text('Cancel'), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + foregroundColor: Colors.white, + ), + onPressed: () => Navigator.of(dialogContext).pop(true), + child: Text('Delete Account'), + ), + ], + ); + }, + ); + + if (confirmed == true && mounted) { + try { + // Show loading indicator + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext loadingContext) { + return Center( + child: CircularProgressIndicator( + color: primaryColor, + ), + ); + }, + ); + + final response = await secureDelete('delete_account'); + + // Remove loading indicator + if (mounted) Navigator.of(context).pop(); + + if (response == null) { + throw Exception('No response from server'); + } + + if (response.statusCode == 200) { + if (!mounted) return; + + // Navigate to splash screen (remove all routes) + Navigator.of(context).pushNamedAndRemoveUntil('/', (route) => false); + } else { + final body = json.decode(response.body); + throw Exception(body['error'] ?? 'Failed to delete account'); + } + } 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)); + } + } + } + + Future _handleChangeEmail() async { + final TextEditingController emailController = TextEditingController(); + final formKey = GlobalKey(); + final primaryColor = Theme.of(context).primaryColorLight; + + final result = await showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: Text('Change Email'), + content: Form( + key: formKey, + child: TextFormField( + controller: emailController, + decoration: InputDecoration( + labelText: 'New Email Address', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.email), + ), + keyboardType: TextInputType.emailAddress, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter an email address'; + } + final emailRegex = RegExp(r'^[^\s@]+@[^\s@]+\.[^\s@]+$'); + if (!emailRegex.hasMatch(value)) { + return 'Please enter a valid email'; + } + if (value == email) { + return 'This is your current email'; + } + return null; + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(false), + child: Text('Cancel'), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + ), + onPressed: () async { + if (formKey.currentState!.validate()) { + Navigator.of(dialogContext).pop(true); + } + }, + child: Text('Send Verification'), + ), + ], + ); + }, + ); + + // Allow dialog to fully close before disposing controller + await Future.delayed(Duration(milliseconds: 100)); + + if (result == true && mounted) { + final newEmail = emailController.text.trim(); + + try { + // Show loading indicator + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext loadingContext) { + return Center( + child: CircularProgressIndicator( + color: primaryColor, + ), + ); + }, + ); + + final localHour = DateTime.now().hour; + final response = await securePost( + { + 'newEmail': newEmail, + 'localHour': localHour, + }, + 'request-email-change', + ); + + // Remove loading indicator + if (mounted) Navigator.of(context).pop(); + + if (response == null) { + throw Exception('No response from server'); + } + + if (response.statusCode == 200) { + if (!mounted) return; + + // Navigate to waiting screen + final success = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EmailChangeWaitingScreen(newEmail: newEmail), + ), + ); + + // If email was changed successfully, refresh account info + if (success == true && mounted) { + await fetchAccountInfo(); + } + } else { + final body = json.decode(response.body); + throw Exception(body['error'] ?? 'Failed to send verification email'); + } + } 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)); + } finally { + emailController.dispose(); + } + } else { + emailController.dispose(); + } + } + + @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: [ + ListTile( + leading: Icon(Icons.email_outlined), + title: Text('Change Email'), + trailing: Icon(Icons.arrow_forward_ios, size: 16), + onTap: _handleChangeEmail, + ), + Divider(height: 1), + ListTile( + leading: Icon(Icons.lock_outline), + title: Text('Change Password'), + trailing: Icon(Icons.arrow_forward_ios, size: 16), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChangePasswordScreen(), + ), + ); + }, + ), + ], + ), + ), + SizedBox(height: 30), + + // Danger Zone Section + Text( + 'Danger Zone', + style: GoogleFonts.aBeeZee( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ), + SizedBox(height: 15), + Card( + elevation: 2, + child: ListTile( + leading: Icon(Icons.delete_forever, color: Colors.red), + title: Text( + 'Delete Account', + style: TextStyle(color: Colors.red), + ), + trailing: Icon(Icons.arrow_forward_ios, size: 16, color: Colors.red), + onTap: _handleDeleteAccount, + ), + ), + ], + ), + ), + ), + ); + } + + 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/change_password_screen.dart b/lib/BlindMasterScreens/accountManagement/change_password_screen.dart similarity index 100% rename from lib/BlindMasterScreens/change_password_screen.dart rename to lib/BlindMasterScreens/accountManagement/change_password_screen.dart diff --git a/lib/BlindMasterScreens/accountManagement/verify_email_change_screen.dart b/lib/BlindMasterScreens/accountManagement/verify_email_change_screen.dart new file mode 100644 index 0000000..3978940 --- /dev/null +++ b/lib/BlindMasterScreens/accountManagement/verify_email_change_screen.dart @@ -0,0 +1,73 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:blind_master/BlindMasterResources/secure_transmissions.dart'; +import 'package:flutter/material.dart'; +import '../email_verification_waiting_screen.dart'; + +class EmailChangeWaitingScreen extends BaseVerificationWaitingScreen { + final String newEmail; + + const EmailChangeWaitingScreen({super.key, required this.newEmail}); + + @override + State createState() => _EmailChangeWaitingScreenState(); +} + +class _EmailChangeWaitingScreenState extends BaseVerificationWaitingScreenState { + @override + String get title => "Verify New Email"; + + @override + String get mainMessage => "We've sent a verification link to:"; + + @override + String? get highlightedInfo => widget.newEmail; + + @override + String get instructionMessage => "Click the link in the email to verify your new email address. This page will automatically update once verified."; + + @override + String get successMessage => "Email changed successfully!"; + + @override + Future checkStatus() async { + final response = await secureGet('pending-email-status'); + + if (response == null) { + throw Exception('No response from server'); + } + + if (response.statusCode == 200) { + final body = json.decode(response.body); + return body['hasPending'] == false; + } + return false; + } + + @override + Future resendVerification() async { + final localHour = DateTime.now().hour; + final response = await securePost( + { + 'newEmail': widget.newEmail, + 'localHour': localHour, + }, + 'request-email-change', + ); + + if (response == null) { + throw Exception('No response from server'); + } + + if (response.statusCode != 200) { + final body = json.decode(response.body); + throw Exception(body['error'] ?? 'Failed to resend verification email'); + } + } + + @override + void onSuccess() { + Navigator.of(context).pop(true); // Return true to indicate success + } +} diff --git a/lib/BlindMasterScreens/account_screen.dart b/lib/BlindMasterScreens/account_screen.dart index ca2634f..5979356 100644 --- a/lib/BlindMasterScreens/account_screen.dart +++ b/lib/BlindMasterScreens/account_screen.dart @@ -1,7 +1,8 @@ import 'dart:convert'; import 'package:blind_master/BlindMasterResources/error_snackbar.dart'; import 'package:blind_master/BlindMasterResources/secure_transmissions.dart'; -import 'package:blind_master/BlindMasterScreens/change_password_screen.dart'; +import 'package:blind_master/BlindMasterScreens/accountManagement/change_password_screen.dart'; +import 'package:blind_master/BlindMasterScreens/accountManagement/verify_email_change_screen.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -66,6 +67,134 @@ class _AccountScreenState extends State { } } + Future _handleChangeEmail() async { + final TextEditingController emailController = TextEditingController(); + final formKey = GlobalKey(); + final primaryColor = Theme.of(context).primaryColorLight; + + final result = await showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + title: Text('Change Email'), + content: Form( + key: formKey, + child: TextFormField( + controller: emailController, + decoration: InputDecoration( + labelText: 'New Email Address', + border: OutlineInputBorder(), + prefixIcon: Icon(Icons.email), + ), + keyboardType: TextInputType.emailAddress, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter an email address'; + } + final emailRegex = RegExp(r'^[^\s@]+@[^\s@]+\.[^\s@]+$'); + if (!emailRegex.hasMatch(value)) { + return 'Please enter a valid email'; + } + if (value == email) { + return 'This is your current email'; + } + return null; + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(false), + child: Text('Cancel'), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: Colors.white, + ), + onPressed: () async { + if (formKey.currentState!.validate()) { + Navigator.of(dialogContext).pop(true); + } + }, + child: Text('Send Verification'), + ), + ], + ); + }, + ); + + // Allow dialog to fully close before disposing controller + await Future.delayed(Duration(milliseconds: 100)); + + if (result == true && mounted) { + final newEmail = emailController.text.trim(); + + try { + // Show loading indicator + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext loadingContext) { + return Center( + child: CircularProgressIndicator( + color: primaryColor, + ), + ); + }, + ); + + final localHour = DateTime.now().hour; + final response = await securePost( + { + 'newEmail': newEmail, + 'localHour': localHour, + }, + 'request-email-change', + ); + + // Remove loading indicator + if (mounted) Navigator.of(context).pop(); + + if (response == null) { + throw Exception('No response from server'); + } + + if (response.statusCode == 200) { + if (!mounted) return; + + // Navigate to waiting screen + final success = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EmailChangeWaitingScreen(newEmail: newEmail), + ), + ); + + // If email was changed successfully, refresh account info + if (success == true && mounted) { + await fetchAccountInfo(); + } + } else { + final body = json.decode(response.body); + throw Exception(body['error'] ?? 'Failed to send verification email'); + } + } 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)); + } finally { + emailController.dispose(); + } + } else { + emailController.dispose(); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -133,6 +262,13 @@ class _AccountScreenState extends State { elevation: 2, child: Column( children: [ + ListTile( + leading: Icon(Icons.email_outlined), + title: Text('Change Email'), + trailing: Icon(Icons.arrow_forward_ios, size: 16), + onTap: _handleChangeEmail, + ), + Divider(height: 1), ListTile( leading: Icon(Icons.lock_outline), title: Text('Change Password'), diff --git a/lib/BlindMasterScreens/Startup/verification_waiting_screen.dart b/lib/BlindMasterScreens/email_verification_waiting_screen.dart similarity index 59% rename from lib/BlindMasterScreens/Startup/verification_waiting_screen.dart rename to lib/BlindMasterScreens/email_verification_waiting_screen.dart index 72d748e..0460884 100644 --- a/lib/BlindMasterScreens/Startup/verification_waiting_screen.dart +++ b/lib/BlindMasterScreens/email_verification_waiting_screen.dart @@ -1,24 +1,27 @@ 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(); +abstract class BaseVerificationWaitingScreen extends StatefulWidget { + const BaseVerificationWaitingScreen({super.key}); } -class _VerificationWaitingScreenState extends State { +abstract class BaseVerificationWaitingScreenState extends State { Timer? _pollingTimer; bool _isChecking = false; + // Abstract methods to be implemented by subclasses + String get title; + String get mainMessage; + String? get highlightedInfo => null; + String get instructionMessage; + String get successMessage; + + Future checkStatus(); + Future resendVerification(); + void onSuccess(); + @override void initState() { super.initState(); @@ -33,11 +36,11 @@ class _VerificationWaitingScreenState extends State { void _startPolling() { _pollingTimer = Timer.periodic(Duration(seconds: 5), (timer) { - _checkVerificationStatus(); + _checkStatus(); }); } - Future _checkVerificationStatus() async { + Future _checkStatus() async { if (_isChecking) return; setState(() { @@ -45,41 +48,30 @@ class _VerificationWaitingScreenState extends State { }); try { - final uri = Uri.parse('https://wahwa.com').replace(path: 'verification_status'); + final isComplete = await checkStatus(); - 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), - ), - ) - ); - } + if (isComplete) { + _pollingTimer?.cancel(); + if (!mounted) return; + + onSuccess(); + + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: Colors.green[800], + duration: Duration(seconds: 4), + content: Text( + successMessage, + 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'); + print('Status check failed: $e'); } finally { if (mounted) { setState(() { @@ -89,40 +81,23 @@ class _VerificationWaitingScreenState extends State { } } - Future _resendVerificationEmail() async { + Future _handleResend() 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)); + await resendVerification(); 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'); - } + 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), + ), + ) + ); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).clearSnackBars(); @@ -133,7 +108,7 @@ class _VerificationWaitingScreenState extends State { } Future _onRefresh() async { - await _checkVerificationStatus(); + await _checkStatus(); } @override @@ -151,12 +126,12 @@ class _VerificationWaitingScreenState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - TitleText("Verify Your Email", txtClr: Colors.white), + TitleText(title, 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", + mainMessage, textAlign: TextAlign.center, style: TextStyle( color: Colors.white, @@ -164,6 +139,21 @@ class _VerificationWaitingScreenState extends State { ), ), ), + if (highlightedInfo != null) ...[ + SizedBox(height: 10), + Padding( + padding: EdgeInsets.symmetric(horizontal: 40), + child: Text( + highlightedInfo!, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + ], SizedBox(height: 20), if (_isChecking) CircularProgressIndicator( @@ -179,7 +169,7 @@ class _VerificationWaitingScreenState extends State { 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.", + instructionMessage, textAlign: TextAlign.center, style: TextStyle( color: Colors.white70, @@ -189,7 +179,7 @@ class _VerificationWaitingScreenState extends State { ), SizedBox(height: 40), ElevatedButton( - onPressed: _resendVerificationEmail, + onPressed: _handleResend, child: Text("Resend Verification Email"), ), SizedBox(height: 20), diff --git a/lib/BlindMasterScreens/home_screen.dart b/lib/BlindMasterScreens/home_screen.dart index 4af0361..c5444ff 100644 --- a/lib/BlindMasterScreens/home_screen.dart +++ b/lib/BlindMasterScreens/home_screen.dart @@ -2,7 +2,7 @@ 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/BlindMasterScreens/accountManagement/account_screen.dart'; import 'package:blind_master/BlindMasterResources/secure_transmissions.dart'; import 'package:blind_master/BlindMasterResources/error_snackbar.dart'; import 'package:flutter/material.dart';