Skip to content

Commit 462a886

Browse files
committed
feat: Add authentication
1 parent fe81696 commit 462a886

24 files changed

+1120
-45
lines changed

.metadata

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ migration:
1515
- platform: root
1616
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
1717
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
18-
- platform: android
18+
- platform: linux
1919
create_revision: 85684f9300908116a78138ea4c6036c35c9a1236
2020
base_revision: 85684f9300908116a78138ea4c6036c35c9a1236
2121

lib/common/form_validator.dart

+32
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,36 @@ class FormValidator {
2424
static String? validateProductQuantity(String? value) {
2525
return value!.isEmpty ? 'Please enter product quantity' : null;
2626
}
27+
28+
//Authentication
29+
static String? validateEmail(String? value) {
30+
String emailPattern =
31+
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';
32+
RegExp regExp = RegExp(emailPattern);
33+
34+
if (value!.isEmpty) {
35+
return 'Please enter your email address';
36+
} else if (!regExp.hasMatch(value)) {
37+
return 'Please enter a valid email';
38+
}
39+
return null;
40+
}
41+
42+
static String? validatePassword(String? value) {
43+
return value!.isEmpty ? 'Please enter your password' : null;
44+
}
45+
46+
static String? validateFullName(String? value) {
47+
return value!.isEmpty ? 'Please enter your fullname' : null;
48+
}
49+
50+
//validatePasswordConfirmation
51+
static String? validatePasswordConfirmation(String? value, String password) {
52+
if (value!.isEmpty) {
53+
return 'Please enter your password confirmation';
54+
} else if (value != password) {
55+
return 'Password confirmation does not match';
56+
}
57+
return null;
58+
}
2759
}

lib/common/providers.dart

+13
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import 'package:dio/dio.dart';
22
import 'package:flutter_riverpod/flutter_riverpod.dart';
33

4+
import '../providers/auth/auth_notifier.dart';
5+
import '../providers/auth/auth_state.dart';
46
import '../providers/quotes_list_notifier.dart';
57
import '../providers/quotes_list_state.dart';
68
import '../providers/submit_quote_notifier.dart';
79
import '../providers/submit_quote_state.dart';
10+
import '../repositories/auth_repository.dart';
811
import '../repositories/quote_repository.dart';
912
import 'constants.dart';
1013

@@ -20,10 +23,16 @@ final dioProvider = Provider<Dio>(
2023
),
2124
);
2225

26+
//Repositories
2327
final repositoryProvider = Provider<QuoteRepository>(
2428
(ref) => QuoteRepositoryImpl(ref.read(dioProvider)),
2529
);
2630

31+
final authRepositoryProvider = Provider<AuthRepository>(
32+
(ref) => AuthRepositoryImpl(ref.read(dioProvider)),
33+
);
34+
35+
//Notifiers
2736
final quotesListNotifier =
2837
StateNotifierProvider<QuotesListNotifier, QuotesListState>(
2938
(ref) => QuotesListNotifier(ref.read(repositoryProvider)),
@@ -36,3 +45,7 @@ final submitQuoteNotifier =
3645
return SubmitQuoteNotifier(ref.read(repositoryProvider), notifier);
3746
},
3847
);
48+
49+
final authNotifier = StateNotifierProvider<AuthNotifier, AuthState>(
50+
(ref) => AuthNotifier(ref.read(authRepositoryProvider)),
51+
);

lib/common/routes.dart

+19-5
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,38 @@
11
import 'package:flutter/cupertino.dart';
22

3-
import '../screens/home_screen.dart';
4-
import '../screens/submit_request_screen.dart';
3+
import '../screens/auth/login_screen.dart';
4+
import '../screens/auth/register_screen.dart';
5+
import '../screens/home/request_list_screen.dart';
6+
import '../screens/home/submit_request_screen.dart';
57

68
Route<dynamic> onGenerate(RouteSettings settings) {
79
switch (settings.name) {
8-
case '/':
10+
case '/request-list':
911
return CupertinoPageRoute(
12+
builder: (_) => const RequestListScreen(),
1013
settings: settings,
11-
builder: (_) => const HomeScreen(),
1214
);
1315
case '/request-quote':
1416
return CupertinoPageRoute(
1517
settings: settings,
1618
builder: (BuildContext context) => const RequestQuoteScreen(),
1719
);
20+
case '/login':
21+
return CupertinoPageRoute(
22+
settings: settings,
23+
builder: (BuildContext context) => const LoginScreen(),
24+
);
25+
case '/register':
26+
return CupertinoPageRoute(
27+
settings: settings,
28+
builder: (BuildContext context) => const RegisterScreen(),
29+
);
1830
default:
1931
return CupertinoPageRoute(
2032
settings: settings,
21-
builder: (_) => const CupertinoPageScaffold(child: SizedBox()),
33+
builder: (_) => const CupertinoPageScaffold(
34+
child: Center(child: Text('Not Found')),
35+
),
2236
);
2337
}
2438
}

lib/main.dart

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_riverpod/flutter_riverpod.dart';
33
import 'package:google_fonts/google_fonts.dart';
4+
import 'package:quote_request_app/screens/auth/login_screen.dart';
45

56
import 'common/routes.dart' as routes;
67

7-
import 'screens/home_screen.dart';
8-
98
void main() {
109
runApp(const QuoteRequestApp());
1110
}
@@ -24,7 +23,7 @@ class QuoteRequestApp extends StatelessWidget {
2423
textTheme: GoogleFonts.dosisTextTheme(),
2524
),
2625
onGenerateRoute: routes.onGenerate,
27-
home: const HomeScreen(),
26+
home: const LoginScreen(),
2827
),
2928
);
3029
}

lib/models/user_model.dart

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import 'package:flutter/foundation.dart';
2+
3+
class UserModel {
4+
final String? uuid;
5+
final String email;
6+
final String? password;
7+
final String fullname;
8+
final String? address;
9+
10+
UserModel({
11+
this.uuid,
12+
this.password,
13+
this.address,
14+
required this.email,
15+
required this.fullname,
16+
});
17+
18+
factory UserModel.fromJson(Map<String, dynamic> json) {
19+
return UserModel(
20+
uuid: json['id'],
21+
email: json['email'],
22+
fullname: json['user_metadata']['fullname'],
23+
address: json['user_metadata']['address'],
24+
);
25+
}
26+
}

lib/providers/auth/auth_notifier.dart

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import 'package:quote_request_app/providers/auth/auth_state.dart';
2+
import 'package:quote_request_app/repositories/auth_repository.dart';
3+
import 'package:riverpod/riverpod.dart';
4+
5+
class AuthNotifier extends StateNotifier<AuthState> {
6+
AuthNotifier(this._authRepository) : super(AuthInitial());
7+
8+
final AuthRepository _authRepository;
9+
10+
Future<void> login(Map<String, dynamic> credentials) async {
11+
state = AuthLoading();
12+
try {
13+
final user = await _authRepository.login(
14+
credentials['email'],
15+
credentials['password'],
16+
);
17+
state = AuthSuccess(user);
18+
} catch (e) {
19+
state = AuthError(e.toString());
20+
}
21+
}
22+
23+
Future<void> register(Map<String, dynamic> userData) async {
24+
state = AuthLoading();
25+
try {
26+
final user = await _authRepository.register(userData);
27+
state = AuthSuccess(user);
28+
} catch (e) {
29+
state = AuthError(e.toString());
30+
}
31+
}
32+
}

lib/providers/auth/auth_state.dart

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import 'package:equatable/equatable.dart';
2+
3+
import '../../models/user_model.dart';
4+
5+
abstract class AuthState extends Equatable {
6+
const AuthState();
7+
}
8+
9+
class AuthInitial extends AuthState {
10+
@override
11+
List<Object> get props => [];
12+
}
13+
14+
class AuthLoading extends AuthState {
15+
@override
16+
List<Object> get props => [];
17+
}
18+
19+
class AuthSuccess extends AuthState {
20+
const AuthSuccess(this.user);
21+
22+
final UserModel user;
23+
24+
@override
25+
List<Object> get props => [user];
26+
}
27+
28+
class AuthError extends AuthState {
29+
final String message;
30+
31+
const AuthError(this.message);
32+
33+
@override
34+
List<Object> get props => [message];
35+
}

lib/repositories/auth_repository.dart

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import 'package:dio/dio.dart';
2+
import 'package:flutter/foundation.dart';
3+
4+
import '../models/user_model.dart';
5+
6+
abstract class AuthRepository {
7+
Future<UserModel> login(String email, String password);
8+
Future<UserModel> register(Map<String, dynamic> userData);
9+
}
10+
11+
class AuthRepositoryImpl implements AuthRepository {
12+
final Dio _dio;
13+
14+
AuthRepositoryImpl(this._dio);
15+
16+
@override
17+
Future<UserModel> login(String email, String password) async {
18+
try {
19+
final response = await _dio.post(
20+
'/auth/v1/token',
21+
queryParameters: {'grant_type': 'password'},
22+
data: {'email': email, 'password': password},
23+
);
24+
25+
if (response.statusCode == 200) {
26+
return UserModel.fromJson(response.data['user']);
27+
} else {
28+
throw Exception('An error occurred');
29+
}
30+
} on DioError catch (e) {
31+
throw await Future.error(e.response!.data['error_description']);
32+
} catch (e) {
33+
debugPrint('AuthRepositoryImpl.login: $e');
34+
throw await Future.error("Couldn't login. Is the device online?");
35+
}
36+
}
37+
38+
@override
39+
Future<UserModel> register(Map<String, dynamic> userData) async {
40+
try {
41+
final response = await _dio.post('/auth/v1/signup', data: userData);
42+
return UserModel.fromJson(response.data);
43+
} on DioError catch (e) {
44+
if (e.response!.statusCode == 400) {
45+
throw await Future.error(e.response!.data['error_description']);
46+
} else {
47+
throw await Future.error(e.message);
48+
}
49+
} catch (e, stack) {
50+
debugPrint('Error: $e\nStack : $stack');
51+
throw await Future.error("Couldn't register. Is the device online?");
52+
}
53+
}
54+
}

lib/repositories/quote_repository.dart

+15-15
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,10 @@ class QuoteRepositoryImpl implements QuoteRepository {
1313

1414
QuoteRepositoryImpl(this._dio);
1515

16-
@override
17-
Future<void> deleteRequestedQuote(int requestedQuoteId) async {
18-
try {
19-
await _dio.delete(
20-
'/requests',
21-
queryParameters: {'id': 'eq.$requestedQuoteId'}, //check PostgREST docs
22-
);
23-
} catch (_) {
24-
throw Future.error("Couldn't delete quote. Is the device online?");
25-
}
26-
}
27-
2816
@override
2917
Future<List<Quote>> getQuotes() async {
3018
try {
31-
final response = await _dio.get('/requests', queryParameters: {
19+
final response = await _dio.get('/rest/v1/requests', queryParameters: {
3220
'order': 'created_at.desc',
3321
});
3422

@@ -43,15 +31,27 @@ class QuoteRepositoryImpl implements QuoteRepository {
4331
@override
4432
Future<Quote> submitQuote(Map<String, dynamic> quote) async {
4533
try {
46-
final response = await _dio.post('/requests', data: quote);
34+
final response = await _dio.post('/rest/v1/requests', data: quote);
4735

4836
if (response.statusCode == 201) {
49-
return Quote.fromMap(response.data[0]);
37+
return Quote.fromMap(response.data.first);
5038
} else {
5139
throw Exception('Error submitting request quote');
5240
}
5341
} catch (_) {
5442
throw Future.error("Couldn't submit quote. Is the device online?");
5543
}
5644
}
45+
46+
@override
47+
Future<void> deleteRequestedQuote(int requestedQuoteId) async {
48+
try {
49+
await _dio.delete(
50+
'/rest/v1/requests',
51+
queryParameters: {'id': 'eq.$requestedQuoteId'}, //check PostgREST docs
52+
);
53+
} catch (_) {
54+
throw Future.error("Couldn't delete quote. Is the device online?");
55+
}
56+
}
5757
}

0 commit comments

Comments
 (0)