Skip to content

Commit 5ec0fcc

Browse files
committed
feat: Add forgot password and reset password page with its functionality; Fix region fetching services; etc.
1 parent c503794 commit 5ec0fcc

17 files changed

Lines changed: 665 additions & 203 deletions

File tree

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
![Android](https://img.shields.io/badge/Android-34A853?style=flat&logo=android&logoColor=white)
66
![Dart](https://img.shields.io/badge/Dart-0175C2?style=flat&logo=dart&logoColor=white)
77
![Flutter](https://img.shields.io/badge/Flutter-02569B?style=flat&logo=flutter&logoColor=white)
8-
![License](https://img.shields.io/badge/License-MIT-green?style=flat)
98

109
</div>
1110

lib/core/services/region.dart

Lines changed: 23 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import 'package:monet/utils/log.dart';
33

44
class Region {
55
final Dio _dio = Dio(BaseOptions(
6-
baseUrl: 'https://restcountries.com/v3.1',
6+
baseUrl: 'https://countriesnow.space/api/v0.1',
77
connectTimeout: const Duration(seconds: 10),
88
receiveTimeout: const Duration(seconds: 10),
99
));
@@ -19,43 +19,27 @@ class Region {
1919
/// ```
2020
Future<List<String>> getCountries() async {
2121
try {
22-
final response = await _dio.get('/all?fields=name');
22+
final response = await _dio.get('/countries/currency');
2323

24-
if (response.statusCode != 200 || response.data == null) {
25-
Log.error('Failed to fetch countries. Status code: ${response.statusCode}');
26-
throw Exception('Failed to fetch countries from the server.');
24+
if (response.statusCode != 200) {
25+
throw Exception('An error occurred while fetching countries. Please try again later.');
2726
}
2827

2928
final dynamic responseData = response.data;
3029

31-
if (responseData is Map) {
32-
Log.warning('API returned a Map instead of List: $responseData');
33-
final message = responseData['message']?.toString();
34-
throw Exception(message ?? 'Unexpected API response format received.');
30+
if (responseData is! Map || responseData['error'] == true) {
31+
Log.error('Unexpected API response format: $responseData');
32+
throw Exception('Unexpected API response format.');
3533
}
3634

37-
if (responseData is! List) {
38-
throw Exception('Invalid data format received from the server.');
39-
}
40-
41-
final List<dynamic> data = responseData;
42-
43-
final List<String> countries = data.map((country) {
44-
if (country is! Map) return '';
45-
if (country['name'] == null) return '';
46-
if (country['name']['common'] == null) return '';
47-
48-
return country['name']['common'] as String;
49-
}).where((name) => name.isNotEmpty).toList();
35+
final List<dynamic> data = responseData['data'];
36+
final List<String> countries = data.map((e) => e['name']?.toString()).where((name) => name != null && name.isNotEmpty).cast<String>().toList();
5037

5138
countries.sort();
5239
return countries;
53-
} on DioException catch (e) {
54-
Log.error('DioException in getCountries: ${e.message}', e);
55-
throw Exception('Connection issue occurred. Please check your internet connection.');
5640
} catch (e) {
57-
Log.error('Error parsing countries: $e');
58-
throw Exception(e.toString().replaceAll('Exception: ', ''));
41+
Log.error('Error fetching countries: $e');
42+
return ['Australia', 'Brazil', 'Canada', 'Indonesia', 'Japan', 'Malaysia', 'Singapore', 'United Kingdom', 'United States'];
5943
}
6044
}
6145

@@ -70,44 +54,32 @@ class Region {
7054
/// ```
7155
Future<List<String>> getCurrencies() async {
7256
try {
73-
final response = await _dio.get('/all?fields=currencies');
57+
final response = await _dio.get('/countries/currency');
7458

75-
if (response.statusCode != 200 || response.data == null) {
76-
Log.error('Failed to fetch currencies. Status code: ${response.statusCode}');
77-
throw Exception('Failed to fetch currencies from the server.');
59+
if (response.statusCode != 200) {
60+
throw Exception('An error occurred while fetching currencies. Please try again later.');
7861
}
7962

8063
final dynamic responseData = response.data;
8164

82-
if (responseData is Map) {
83-
Log.warning('API returned a Map instead of List: $responseData');
84-
final message = responseData['message']?.toString();
85-
throw Exception(message ?? 'Unexpected API response format received.');
65+
if (responseData is! Map || responseData['error'] == true) {
66+
Log.error('Unexpected API response format: $responseData');
67+
throw Exception('Unexpected API response format.');
8668
}
8769

88-
if (responseData is! List) {
89-
throw Exception('Invalid data format received from the server.');
90-
}
91-
92-
final List<dynamic> data = responseData;
70+
final List<dynamic> data = responseData['data'];
9371
final Set<String> currencies = {};
9472

9573
for (final country in data) {
96-
if (country is! Map) continue;
97-
98-
final Map<String, dynamic>? currencyMap = country['currencies'];
99-
if (currencyMap == null) continue;
100-
101-
currencies.addAll(currencyMap.keys);
74+
final currency = country['currency']?.toString();
75+
if (currency == null || currency.isEmpty) continue;
76+
currencies.add(currency);
10277
}
10378

10479
return currencies.toList()..sort();
105-
} on DioException catch (e) {
106-
Log.error('DioException in getCurrencies: ${e.message}', e);
107-
throw Exception('Connection issue occurred. Please check your internet connection.');
10880
} catch (e) {
109-
Log.error('Error parsing currencies: $e');
110-
throw Exception(e.toString().replaceAll('Exception: ', ''));
81+
Log.error('Error fetching currencies: $e');
82+
return ['AUD', 'BRL', 'CAD', 'EUR', 'GBP', 'IDR', 'JPY', 'MYR', 'SGD', 'USD'];
11183
}
11284
}
11385
}

lib/features/auth/forgot-password/forgot_password_page.dart

Lines changed: 0 additions & 13 deletions
This file was deleted.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:google_fonts/google_fonts.dart';
3+
import 'package:monet/features/auth/forgot_password/widgets/forgot_password_form.dart';
4+
import 'package:monet/routes/app.dart';
5+
6+
class ForgotPasswordPage extends StatelessWidget {
7+
const ForgotPasswordPage({super.key});
8+
9+
@override
10+
Widget build(BuildContext context) {
11+
return Scaffold(
12+
backgroundColor: const Color(0xFFF8FAFC),
13+
body: SafeArea(
14+
child: Center(
15+
child: SingleChildScrollView(
16+
padding: const EdgeInsets.all(24.0),
17+
child: Column(
18+
crossAxisAlignment: CrossAxisAlignment.stretch,
19+
mainAxisAlignment: MainAxisAlignment.center,
20+
children: [
21+
Image(
22+
height: 100,
23+
image: const AssetImage('logo.png'),
24+
semanticLabel: 'Monet Logo',
25+
width: 100,
26+
errorBuilder: (context, error, stackTrace) => const Icon(
27+
Icons.account_balance_wallet_outlined,
28+
color: Color(0xFF4F46E5),
29+
size: 64,
30+
),
31+
),
32+
const SizedBox(height: 16),
33+
Text(
34+
'Forgot Password',
35+
style: GoogleFonts.poppins(
36+
color: const Color(0xFF0F172A),
37+
fontSize: 28,
38+
fontWeight: FontWeight.bold,
39+
),
40+
textAlign: TextAlign.center,
41+
),
42+
const SizedBox(height: 8),
43+
Text(
44+
'Please enter your email address to reset your password.',
45+
style: GoogleFonts.poppins(
46+
color: Colors.grey.shade600,
47+
fontSize: 14,
48+
height: 1.5,
49+
),
50+
textAlign: TextAlign.center,
51+
),
52+
const SizedBox(height: 48),
53+
Container(
54+
decoration: BoxDecoration(
55+
borderRadius: BorderRadius.circular(20),
56+
color: Colors.white,
57+
boxShadow: [
58+
BoxShadow(
59+
blurRadius: 20,
60+
color: Colors.black.withValues(alpha: 0.03),
61+
offset: const Offset(0, 10),
62+
),
63+
],
64+
),
65+
padding: const EdgeInsets.all(24.0),
66+
child: const ForgotPasswordForm(),
67+
),
68+
const SizedBox(height: 24),
69+
Row(
70+
mainAxisAlignment: MainAxisAlignment.center,
71+
children: [
72+
Text(
73+
'Remember your password?',
74+
style: GoogleFonts.poppins(color: Colors.grey.shade600),
75+
),
76+
TextButton(
77+
onPressed: () => LoginRoute().push(context),
78+
child: Text(
79+
'Back to Login',
80+
style: GoogleFonts.poppins(
81+
color: const Color(0xFF4F46E5),
82+
fontWeight: FontWeight.w600,
83+
),
84+
),
85+
),
86+
],
87+
),
88+
],
89+
),
90+
),
91+
),
92+
),
93+
);
94+
}
95+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import 'package:dio/dio.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:go_router/go_router.dart';
4+
import 'package:monet/constants/routes.dart';
5+
import 'package:monet/utils/log.dart';
6+
7+
/// Service class to handle forgot password functionality, including sending a
8+
/// password reset request to the backend API and handling responses.
9+
class ForgotPasswordService {
10+
late final Dio _dio = Dio(BaseOptions(
11+
baseUrl: const String.fromEnvironment('API_BASE_URL', defaultValue: 'http://localhost:8000/'),
12+
connectTimeout: const Duration(seconds: 10),
13+
receiveTimeout: const Duration(seconds: 10),
14+
));
15+
16+
Future<void> handleForgotPassword({ required BuildContext context, required String email }) async {
17+
try {
18+
final response = await _dio.post(Routes.forgotPassword, data: {'email': email});
19+
20+
if (response.statusCode != 200) {
21+
throw Exception('Failed to send reset link. Please try again.');
22+
}
23+
24+
if (!context.mounted) return;
25+
26+
ScaffoldMessenger.of(context).showSnackBar(
27+
const SnackBar(
28+
backgroundColor: Colors.green,
29+
content: Text('Password reset link has been sent to your email.'),
30+
),
31+
);
32+
33+
context.go(Routes.login);
34+
} on DioException catch (e) {
35+
String errorMessage = 'An unexpected error occurred.';
36+
final statusCode = e.response?.statusCode;
37+
38+
if (e.response == null) {
39+
Log.error('Network Error: ${e.message}', e);
40+
errorMessage = 'Network error. Please check your internet connection.';
41+
}
42+
43+
if (statusCode == 404) {
44+
errorMessage = 'This email is not registered in our system.';
45+
}
46+
47+
if (statusCode == 500) {
48+
errorMessage = 'Our servers are currently experiencing issues. Please try again later.';
49+
}
50+
51+
if (!context.mounted) return;
52+
53+
ScaffoldMessenger.of(context).showSnackBar(
54+
SnackBar(
55+
backgroundColor: Colors.redAccent,
56+
content: Text(errorMessage),
57+
),
58+
);
59+
} catch (e) {
60+
if (!context.mounted) return;
61+
62+
ScaffoldMessenger.of(context).showSnackBar(
63+
SnackBar(
64+
backgroundColor: Colors.redAccent,
65+
content: Text(e.toString().replaceAll('Exception: ', '')),
66+
),
67+
);
68+
}
69+
}
70+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:google_fonts/google_fonts.dart';
3+
import 'package:monet/helpers/input_style.dart';
4+
import 'package:monet/features/auth/forgot_password/services/forgot_password_service.dart';
5+
6+
class ForgotPasswordForm extends StatefulWidget {
7+
const ForgotPasswordForm({super.key});
8+
9+
@override
10+
State<ForgotPasswordForm> createState() => _ForgotPasswordFormState();
11+
}
12+
13+
class _ForgotPasswordFormState extends State<ForgotPasswordForm> {
14+
final _formKey = GlobalKey<FormState>();
15+
final _emailController = TextEditingController();
16+
17+
final _inputStyle = InputStyle();
18+
final _forgotPasswordService = ForgotPasswordService();
19+
20+
bool _isLoading = false;
21+
22+
@override
23+
void dispose() {
24+
_emailController.dispose();
25+
super.dispose();
26+
}
27+
28+
void _handleSubmit() async {
29+
if (!_formKey.currentState!.validate()) return;
30+
31+
setState(() => _isLoading = true);
32+
await _forgotPasswordService.handleForgotPassword(context: context, email: _emailController.text);
33+
34+
if (mounted) setState(() => _isLoading = false);
35+
}
36+
37+
@override
38+
Widget build(BuildContext context) {
39+
return Form(
40+
key: _formKey,
41+
child: Column(
42+
crossAxisAlignment: CrossAxisAlignment.stretch,
43+
children: [
44+
TextFormField(
45+
controller: _emailController,
46+
decoration: _inputStyle.input('Email Address', Icons.email_outlined),
47+
keyboardType: TextInputType.emailAddress,
48+
style: GoogleFonts.poppins(color: const Color(0xFF0F172A)),
49+
validator: (value) {
50+
if (value == null || value.isEmpty) return 'Please enter your email';
51+
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) return 'Please enter a valid email';
52+
return null;
53+
},
54+
),
55+
const SizedBox(height: 32),
56+
ElevatedButton(
57+
onPressed: _isLoading ? null : _handleSubmit,
58+
style: ElevatedButton.styleFrom(
59+
backgroundColor: const Color(0xFF4F46E5),
60+
elevation: 0,
61+
padding: const EdgeInsets.symmetric(vertical: 16),
62+
minimumSize: const Size.fromHeight(56),
63+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
64+
),
65+
child: _isLoading
66+
? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2))
67+
: Text('Send Reset Link', style: GoogleFonts.poppins(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600)),
68+
),
69+
],
70+
),
71+
);
72+
}
73+
}

0 commit comments

Comments
 (0)