Initial Flutter scaffold: Riverpod + GoRouter + Dio
This commit is contained in:
115
lib/shared/providers/auth_provider.dart
Normal file
115
lib/shared/providers/auth_provider.dart
Normal file
@@ -0,0 +1,115 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../services/api_client.dart';
|
||||
|
||||
class AuthState {
|
||||
final bool isAuthenticated;
|
||||
final Map<String, dynamic>? user;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
|
||||
const AuthState({
|
||||
this.isAuthenticated = false,
|
||||
this.user,
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
});
|
||||
|
||||
AuthState copyWith({
|
||||
bool? isAuthenticated,
|
||||
Map<String, dynamic>? user,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
}) {
|
||||
return AuthState(
|
||||
isAuthenticated: isAuthenticated ?? this.isAuthenticated,
|
||||
user: user ?? this.user,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AuthNotifier extends StateNotifier<AsyncValue<AuthState>> {
|
||||
final ApiClient _apiClient;
|
||||
|
||||
AuthNotifier(this._apiClient) : super(const AsyncValue.loading()) {
|
||||
_checkSession();
|
||||
}
|
||||
|
||||
Future<void> _checkSession() async {
|
||||
try {
|
||||
final session = await _apiClient.getSession();
|
||||
if (session != null && session['user'] != null) {
|
||||
state = AsyncValue.data(AuthState(
|
||||
isAuthenticated: true,
|
||||
user: session['user'],
|
||||
));
|
||||
} else {
|
||||
state = const AsyncValue.data(AuthState(isAuthenticated: false));
|
||||
}
|
||||
} catch (e) {
|
||||
state = const AsyncValue.data(AuthState(isAuthenticated: false));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> signUp({
|
||||
required String email,
|
||||
required String password,
|
||||
required String name,
|
||||
}) async {
|
||||
state = const AsyncValue.loading();
|
||||
try {
|
||||
final result = await _apiClient.signUp(
|
||||
email: email,
|
||||
password: password,
|
||||
name: name,
|
||||
);
|
||||
state = AsyncValue.data(AuthState(
|
||||
isAuthenticated: true,
|
||||
user: result['user'],
|
||||
));
|
||||
} catch (e) {
|
||||
state = AsyncValue.data(AuthState(
|
||||
isAuthenticated: false,
|
||||
error: e.toString(),
|
||||
));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> signIn({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
state = const AsyncValue.loading();
|
||||
try {
|
||||
final result = await _apiClient.signIn(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
state = AsyncValue.data(AuthState(
|
||||
isAuthenticated: true,
|
||||
user: result['user'],
|
||||
));
|
||||
} catch (e) {
|
||||
state = AsyncValue.data(AuthState(
|
||||
isAuthenticated: false,
|
||||
error: e.toString(),
|
||||
));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> signOut() async {
|
||||
try {
|
||||
await _apiClient.signOut();
|
||||
} finally {
|
||||
state = const AsyncValue.data(AuthState(isAuthenticated: false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final authStateProvider = StateNotifierProvider<AuthNotifier, AsyncValue<AuthState>>((ref) {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
return AuthNotifier(apiClient);
|
||||
});
|
||||
182
lib/shared/services/api_client.dart
Normal file
182
lib/shared/services/api_client.dart
Normal file
@@ -0,0 +1,182 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import '../../config/env.dart';
|
||||
|
||||
final apiClientProvider = Provider<ApiClient>((ref) {
|
||||
return ApiClient();
|
||||
});
|
||||
|
||||
class ApiClient {
|
||||
late final Dio _dio;
|
||||
final _storage = const FlutterSecureStorage();
|
||||
|
||||
ApiClient() {
|
||||
_dio = Dio(BaseOptions(
|
||||
baseUrl: Env.apiBaseUrl,
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
receiveTimeout: const Duration(seconds: 30),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
));
|
||||
|
||||
_dio.interceptors.add(InterceptorsWrapper(
|
||||
onRequest: (options, handler) async {
|
||||
// Add auth token if available
|
||||
final token = await _storage.read(key: 'session_token');
|
||||
if (token != null) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
handler.next(options);
|
||||
},
|
||||
onError: (error, handler) {
|
||||
if (error.response?.statusCode == 401) {
|
||||
// Handle unauthorized - clear token and redirect to login
|
||||
_storage.delete(key: 'session_token');
|
||||
}
|
||||
handler.next(error);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
// Auth
|
||||
Future<Map<String, dynamic>> signUp({
|
||||
required String email,
|
||||
required String password,
|
||||
required String name,
|
||||
}) async {
|
||||
final response = await _dio.post('/api/auth/sign-up/email', data: {
|
||||
'email': email,
|
||||
'password': password,
|
||||
'name': name,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> signIn({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
final response = await _dio.post('/api/auth/sign-in/email', data: {
|
||||
'email': email,
|
||||
'password': password,
|
||||
});
|
||||
|
||||
// Store session token
|
||||
if (response.data['token'] != null) {
|
||||
await _storage.write(key: 'session_token', value: response.data['token']);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
Future<void> signOut() async {
|
||||
await _dio.post('/api/auth/sign-out');
|
||||
await _storage.delete(key: 'session_token');
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>?> getSession() async {
|
||||
try {
|
||||
final response = await _dio.get('/api/auth/session');
|
||||
return response.data;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Clients
|
||||
Future<List<Map<String, dynamic>>> getClients({String? search, String? tag}) async {
|
||||
final response = await _dio.get('/api/clients', queryParameters: {
|
||||
if (search != null) 'search': search,
|
||||
if (tag != null) 'tag': tag,
|
||||
});
|
||||
return List<Map<String, dynamic>>.from(response.data);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getClient(String id) async {
|
||||
final response = await _dio.get('/api/clients/$id');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> createClient(Map<String, dynamic> data) async {
|
||||
final response = await _dio.post('/api/clients', data: data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> updateClient(String id, Map<String, dynamic> data) async {
|
||||
final response = await _dio.put('/api/clients/$id', data: data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
Future<void> deleteClient(String id) async {
|
||||
await _dio.delete('/api/clients/$id');
|
||||
}
|
||||
|
||||
Future<void> markClientContacted(String id) async {
|
||||
await _dio.post('/api/clients/$id/contacted');
|
||||
}
|
||||
|
||||
// Emails
|
||||
Future<Map<String, dynamic>> generateEmail({
|
||||
required String clientId,
|
||||
required String purpose,
|
||||
String? provider,
|
||||
}) async {
|
||||
final response = await _dio.post('/api/emails/generate', data: {
|
||||
'clientId': clientId,
|
||||
'purpose': purpose,
|
||||
if (provider != null) 'provider': provider,
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getEmails({String? status, String? clientId}) async {
|
||||
final response = await _dio.get('/api/emails', queryParameters: {
|
||||
if (status != null) 'status': status,
|
||||
if (clientId != null) 'clientId': clientId,
|
||||
});
|
||||
return List<Map<String, dynamic>>.from(response.data);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> updateEmail(String id, Map<String, dynamic> data) async {
|
||||
final response = await _dio.put('/api/emails/$id', data: data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> sendEmail(String id) async {
|
||||
final response = await _dio.post('/api/emails/$id/send');
|
||||
return response.data;
|
||||
}
|
||||
|
||||
Future<void> deleteEmail(String id) async {
|
||||
await _dio.delete('/api/emails/$id');
|
||||
}
|
||||
|
||||
// Events
|
||||
Future<List<Map<String, dynamic>>> getEvents({
|
||||
String? clientId,
|
||||
String? type,
|
||||
int? upcomingDays,
|
||||
}) async {
|
||||
final response = await _dio.get('/api/events', queryParameters: {
|
||||
if (clientId != null) 'clientId': clientId,
|
||||
if (type != null) 'type': type,
|
||||
if (upcomingDays != null) 'upcoming': upcomingDays.toString(),
|
||||
});
|
||||
return List<Map<String, dynamic>>.from(response.data);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> createEvent(Map<String, dynamic> data) async {
|
||||
final response = await _dio.post('/api/events', data: data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
Future<void> deleteEvent(String id) async {
|
||||
await _dio.delete('/api/events/$id');
|
||||
}
|
||||
|
||||
Future<void> syncClientEvents(String clientId) async {
|
||||
await _dio.post('/api/events/sync/$clientId');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user