- Read bearer token from set-auth-token header - Add mounted checks to prevent setState after dispose - Add mocktail for testing - Add widget tests for login, clients, events screens - Add unit tests for auth provider, API client - 110 tests passing
191 lines
5.5 KiB
Dart
191 lines
5.5 KiB
Dart
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,
|
|
});
|
|
|
|
// Store session token from header (BetterAuth auto-signs in after signup)
|
|
final authToken = response.headers.value('set-auth-token');
|
|
if (authToken != null) {
|
|
await _storage.write(key: 'session_token', value: authToken);
|
|
}
|
|
|
|
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 from header (BetterAuth bearer plugin)
|
|
final authToken = response.headers.value('set-auth-token');
|
|
if (authToken != null) {
|
|
await _storage.write(key: 'session_token', value: authToken);
|
|
}
|
|
|
|
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');
|
|
}
|
|
}
|