feat: migrate prosappco from Firebase to NestJS REST API (Fase 2)
- Replace all Firebase* repositories with Api* repositories using HTTP + SharedPreferences JWT - Remove Firebase.initializeApp() and firebase_messaging background handler from main.dart - Update DI (app_di.dart) to inject Api* repositories instead of Firebase* ones - Replace all Timestamp/cloud_firestore usage with ISO 8601 String dates - Stub PhoneVerificationService (Firebase phone OTP → backend OTP when implemented) - Add ApiService singleton with JWT management in lib/services/ - Legacy firebase_*_repository.dart files preserved for Fase 4 cleanup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
726cf12fd2
commit
733384091c
@@ -2,3 +2,4 @@ library chat_repository;
|
||||
|
||||
export 'src/entities/entities.dart';
|
||||
export 'src/repositories/firebase_chat_repository.dart';
|
||||
export 'src/repositories/api_chat_repository.dart';
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:chat_repository/chat_repository.dart';
|
||||
|
||||
const _base = 'https://backend.prosapp.co/api/v1';
|
||||
|
||||
/// API-backed replacement for FirebaseChatRepository.
|
||||
/// Mirrors the same public API so existing blocs work without changes.
|
||||
class ApiChatRepository {
|
||||
String? _token;
|
||||
|
||||
Future<String?> _getToken() async {
|
||||
if (_token != null) return _token;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return _token = prefs.getString('token');
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _headers() async {
|
||||
final t = await _getToken();
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
if (t != null) 'Authorization': 'Bearer $t',
|
||||
};
|
||||
}
|
||||
|
||||
ChatEntity _chatFromApi(Map<String, dynamic> json) {
|
||||
final rawMessages = json['messages'] as List? ?? [];
|
||||
final messages = rawMessages
|
||||
.map((m) => MessageEntity.fromDocument(m as Map<String, dynamic>))
|
||||
.toList();
|
||||
return ChatEntity(
|
||||
id: json['id']?.toString(),
|
||||
userId: json['user_id']?.toString() ?? json['userId']?.toString() ?? '',
|
||||
professionalId: json['professional_id']?.toString() ??
|
||||
json['professionalId']?.toString() ??
|
||||
'',
|
||||
messages: messages,
|
||||
);
|
||||
}
|
||||
|
||||
MessageEntity _msgFromApi(Map<String, dynamic> json) {
|
||||
return MessageEntity(
|
||||
ownerId: json['owner_id']?.toString() ?? json['sender_id']?.toString() ?? '',
|
||||
content: json['content']?.toString() ?? '',
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.tryParse(json['created_at'].toString()) ?? DateTime.now()
|
||||
: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Get or create a chat session. Maps to POST /chat/start/:professionalUserId.
|
||||
/// [chatId] here is used as the professional's userId for the REST call.
|
||||
Future<ChatEntity> createNewChat(
|
||||
String chatId, String userId, String professionalId) async {
|
||||
final res = await http.post(
|
||||
Uri.parse('$_base/chat/start/$professionalId'),
|
||||
headers: await _headers(),
|
||||
);
|
||||
final data = jsonDecode(res.body) as Map<String, dynamic>;
|
||||
return _chatFromApi(data);
|
||||
}
|
||||
|
||||
/// Streams a single chat by its ID. Fetches once and emits.
|
||||
Stream<ChatEntity?> getChatById(String chatId) {
|
||||
final controller = StreamController<ChatEntity?>();
|
||||
_fetchChat(chatId).then((chat) {
|
||||
controller.add(chat);
|
||||
controller.close();
|
||||
}).catchError((e) {
|
||||
controller.add(null);
|
||||
controller.close();
|
||||
});
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
Future<ChatEntity?> _fetchChat(String chatId) async {
|
||||
try {
|
||||
// Try to get messages for this chat — if the chat exists it'll succeed
|
||||
final res = await http.get(
|
||||
Uri.parse('$_base/chat/$chatId/messages'),
|
||||
headers: await _headers(),
|
||||
);
|
||||
if (res.statusCode == 404) return null;
|
||||
final messages = jsonDecode(res.body) as List? ?? [];
|
||||
return ChatEntity(
|
||||
id: chatId,
|
||||
userId: '',
|
||||
professionalId: '',
|
||||
messages: messages
|
||||
.map((m) => _msgFromApi(m as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendMessage(String chatId, MessageEntity message) async {
|
||||
await http.post(
|
||||
Uri.parse('$_base/chat/$chatId/message'),
|
||||
headers: await _headers(),
|
||||
body: jsonEncode({'content': message.content}),
|
||||
);
|
||||
}
|
||||
|
||||
/// Get all chats for the current user.
|
||||
Future<List<ChatEntity>> getMyChats() async {
|
||||
try {
|
||||
final res = await http.get(
|
||||
Uri.parse('$_base/chat/my'),
|
||||
headers: await _headers(),
|
||||
);
|
||||
final data = jsonDecode(res.body) as List? ?? [];
|
||||
return data.map((e) => _chatFromApi(e as Map<String, dynamic>)).toList();
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,10 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
equatable: ^2.0.5
|
||||
http: ^1.1.0
|
||||
shared_preferences: ^2.0.10
|
||||
|
||||
# Firebase
|
||||
# Firebase kept for FirebaseChatRepository (legacy fallback)
|
||||
cloud_firestore: ^4.15.4
|
||||
firebase_core: ^2.25.4
|
||||
|
||||
@@ -22,4 +24,4 @@ dev_dependencies:
|
||||
sdk: flutter
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
uses-material-design: true
|
||||
|
||||
@@ -4,3 +4,4 @@ export 'src/models/models.dart';
|
||||
export 'src/entities/entities.dart';
|
||||
export 'src/repositories/city_repo.dart';
|
||||
export 'src/repositories/firebase_city_repository.dart';
|
||||
export 'src/repositories/api_city_repository.dart';
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:city_repository/city_repository.dart';
|
||||
import 'city_repo.dart';
|
||||
|
||||
const _base = 'https://backend.prosapp.co/api/v1';
|
||||
|
||||
class ApiCityRepository implements CityRepository {
|
||||
List<CityUi>? _cities;
|
||||
|
||||
Future<String?> _getToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString('token');
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _headers() async {
|
||||
final t = await _getToken();
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
if (t != null) 'Authorization': 'Bearer $t',
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<CityUi>> getCities() async {
|
||||
if (_cities != null) return _cities!;
|
||||
|
||||
final List<CityUi> cities = [];
|
||||
|
||||
try {
|
||||
final countriesRes = await http.get(
|
||||
Uri.parse('$_base/locations/countries'),
|
||||
headers: await _headers(),
|
||||
);
|
||||
final countries = jsonDecode(countriesRes.body) as List;
|
||||
|
||||
for (final country in countries) {
|
||||
final countryId = country['id']?.toString() ?? '';
|
||||
final countryName = country['name']?.toString() ?? '';
|
||||
|
||||
final regionsRes = await http.get(
|
||||
Uri.parse('$_base/locations/countries/$countryId/regions'),
|
||||
headers: await _headers(),
|
||||
);
|
||||
final regions = jsonDecode(regionsRes.body) as List;
|
||||
|
||||
for (final region in regions) {
|
||||
final regionId = region['id']?.toString() ?? '';
|
||||
final regionName = region['name']?.toString() ?? '';
|
||||
|
||||
final citiesRes = await http.get(
|
||||
Uri.parse('$_base/locations/regions/$regionId/cities'),
|
||||
headers: await _headers(),
|
||||
);
|
||||
final citiesList = jsonDecode(citiesRes.body) as List;
|
||||
|
||||
for (final city in citiesList) {
|
||||
cities.add(CityUi(
|
||||
cityName: city['name']?.toString() ?? '',
|
||||
coordsOfCity: city['coords']?.toString() ?? '',
|
||||
stateOfCity: regionName,
|
||||
countryOfCity: countryName,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
_cities = cities;
|
||||
return cities;
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,10 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
equatable: ^2.0.5
|
||||
http: ^1.1.0
|
||||
shared_preferences: ^2.0.10
|
||||
|
||||
# Firebase
|
||||
# Firebase kept for FirebaseCityRepository (legacy fallback)
|
||||
cloud_firestore: ^4.15.4
|
||||
firebase_core: ^2.25.4
|
||||
|
||||
@@ -23,4 +25,4 @@ dev_dependencies:
|
||||
sdk: flutter
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
uses-material-design: true
|
||||
|
||||
@@ -3,4 +3,5 @@ library profession_repository;
|
||||
export 'src/models/models.dart';
|
||||
export 'src/entities/entities.dart';
|
||||
export 'src/repositories/profession_repo.dart';
|
||||
export 'src/repositories/firebase_profession_repository.dart';
|
||||
export 'src/repositories/firebase_profession_repository.dart';
|
||||
export 'src/repositories/api_profession_repository.dart';
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:profession_repository/profession_repository.dart';
|
||||
import 'profession_repo.dart';
|
||||
|
||||
const _base = 'https://backend.prosapp.co/api/v1';
|
||||
|
||||
class ApiProfessionRepository implements ProfessionRepository {
|
||||
Future<String?> _getToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString('token');
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _headers() async {
|
||||
final t = await _getToken();
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
if (t != null) 'Authorization': 'Bearer $t',
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Professions> getProfessions() async {
|
||||
try {
|
||||
final res = await http.get(
|
||||
Uri.parse('$_base/professions'),
|
||||
headers: await _headers(),
|
||||
);
|
||||
final data = jsonDecode(res.body);
|
||||
// Backend returns either a list of profession objects or a map with a professions key
|
||||
if (data is List) {
|
||||
final names = data
|
||||
.map((e) => (e['name'] ?? e['title'] ?? e.toString()).toString())
|
||||
.toList();
|
||||
return Professions(names);
|
||||
} else if (data is Map && data.containsKey('professions')) {
|
||||
return Professions(List<String>.from(data['professions']));
|
||||
}
|
||||
return Professions([]);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,10 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
equatable: ^2.0.5
|
||||
http: ^1.1.0
|
||||
shared_preferences: ^2.0.10
|
||||
|
||||
# Firebase
|
||||
# Firebase kept for FirebaseProfessionRepository (legacy fallback)
|
||||
cloud_firestore: ^4.15.4
|
||||
firebase_core: ^2.25.4
|
||||
|
||||
@@ -23,4 +25,4 @@ dev_dependencies:
|
||||
sdk: flutter
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
uses-material-design: true
|
||||
|
||||
@@ -3,3 +3,4 @@ library professional_repository;
|
||||
export 'src/models/models.dart';
|
||||
export 'src/entities/entities.dart';
|
||||
export 'src/repositories/firebase_professional_repository.dart';
|
||||
export 'src/repositories/api_professional_repository.dart';
|
||||
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:professional_repository/professional_repository.dart';
|
||||
|
||||
const _base = 'https://backend.prosapp.co/api/v1';
|
||||
|
||||
/// API-backed replacement for FirebaseProfessionalRepository.
|
||||
/// Mirrors the same public API so existing blocs work without changes.
|
||||
class ApiProfessionalRepository {
|
||||
ProfessionalEntity? _proInfo;
|
||||
final StreamController<ProfessionalEntity?> _proInfoBroadcast =
|
||||
StreamController<ProfessionalEntity?>.broadcast();
|
||||
|
||||
bool isProModeActive = false;
|
||||
final StreamController<bool> _isProModeActiveBroadcast =
|
||||
StreamController<bool>.broadcast();
|
||||
|
||||
String? _token;
|
||||
|
||||
ApiProfessionalRepository() {
|
||||
_isProModeActiveBroadcast.add(isProModeActive);
|
||||
}
|
||||
|
||||
Future<String?> _getToken() async {
|
||||
if (_token != null) return _token;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return _token = prefs.getString('token');
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _headers() async {
|
||||
final t = await _getToken();
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
if (t != null) 'Authorization': 'Bearer $t',
|
||||
};
|
||||
}
|
||||
|
||||
Future<dynamic> _get(String path) async {
|
||||
final res = await http.get(Uri.parse('$_base$path'), headers: await _headers());
|
||||
return jsonDecode(res.body);
|
||||
}
|
||||
|
||||
Future<dynamic> _patch(String path, Map<String, dynamic> body) async {
|
||||
final res = await http.patch(
|
||||
Uri.parse('$_base$path'),
|
||||
headers: await _headers(),
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
return jsonDecode(res.body);
|
||||
}
|
||||
|
||||
ProfessionalEntity? lastProInfo() => _proInfo;
|
||||
|
||||
Stream<ProfessionalEntity?> streamProInfo() => _proInfoBroadcast.stream;
|
||||
|
||||
Stream<bool> sreamIsProModeActive() => _isProModeActiveBroadcast.stream;
|
||||
|
||||
switchProMode() {
|
||||
isProModeActive = !isProModeActive;
|
||||
_isProModeActiveBroadcast.add(isProModeActive);
|
||||
}
|
||||
|
||||
ProfessionalEntity _fromApi(Map<String, dynamic> json) {
|
||||
return ProfessionalEntity(
|
||||
id: json['user_id']?.toString() ?? json['id']?.toString() ?? '',
|
||||
identification: json['identification']?.toString() ?? '',
|
||||
address: json['address']?.toString() ?? '',
|
||||
aditionalAddress: json['aditional_address']?.toString() ?? '',
|
||||
profession: json['profession']?.toString() ?? '',
|
||||
ratePreferences: json['rate_preferences'] as bool? ?? false,
|
||||
rate: json['rate']?.toString() ?? '',
|
||||
locationPreferences: intToEnum((json['location_preferences'] as num?)?.toInt() ?? 0),
|
||||
bannerPicture: json['banner_picture']?.toString() ?? '',
|
||||
identificationPicture: json['identification_picture']?.toString() ?? '',
|
||||
certificatePicture: json['certificate_picture']?.toString() ?? '',
|
||||
latitude: double.tryParse(json['latitude']?.toString() ?? '0') ?? 0.0,
|
||||
longitude: double.tryParse(json['longitude']?.toString() ?? '0') ?? 0.0,
|
||||
specializations: json['specializations'] != null
|
||||
? List<String>.from(json['specializations'])
|
||||
: [],
|
||||
specializationsPictures: json['specializations_pictures'] != null
|
||||
? List<String>.from(json['specializations_pictures'])
|
||||
: [],
|
||||
schedules: json['schedules'] != null
|
||||
? Schedules.fromDocument(json['schedules'] as Map<String, dynamic>)
|
||||
: Schedules.empty,
|
||||
paymentMethods: json['payment_methods'] != null
|
||||
? PaymentMethodEntity.fromDocument(
|
||||
json['payment_methods'] as Map<String, dynamic>)
|
||||
: PaymentMethodEntity.empty,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateFromFirebase({required String userId}) async {
|
||||
try {
|
||||
final proInfo = await getProInfo(userId);
|
||||
_proInfo = proInfo;
|
||||
_proInfoBroadcast.add(proInfo);
|
||||
} catch (e) {
|
||||
_proInfo = null;
|
||||
_proInfoBroadcast.add(null);
|
||||
}
|
||||
}
|
||||
|
||||
Future<ProfessionalEntity?> getProInfo(String myUserId) async {
|
||||
try {
|
||||
final data = await _get('/professionals/$myUserId');
|
||||
if (data == null) return null;
|
||||
return _fromApi(data as Map<String, dynamic>);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateProfessionalInfo(
|
||||
String address,
|
||||
String aditionalAddress,
|
||||
bool ratePreferences,
|
||||
String rate,
|
||||
LocationPreferences locationPreferences,
|
||||
double latitude,
|
||||
double longitude,
|
||||
Schedules schedules,
|
||||
PaymentMethodEntity paymentMethods,
|
||||
) async {
|
||||
await _patch('/professionals/me', {
|
||||
'address': address,
|
||||
'aditional_address': aditionalAddress,
|
||||
'rate_preferences': ratePreferences,
|
||||
'rate': rate,
|
||||
'location_preferences': enumToInt(locationPreferences),
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'schedules': schedules.toJson(),
|
||||
'payment_methods': paymentMethods.toDocument(),
|
||||
});
|
||||
|
||||
if (_proInfo != null) {
|
||||
await updateFromFirebase(userId: _proInfo!.id);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveProfessionalInfo(ProfessionalEntity entity) async {
|
||||
await _patch('/professionals/me', entity.toDocument());
|
||||
}
|
||||
|
||||
Future<void> uploadBannerPicture(String file) async {
|
||||
final token = await _getToken();
|
||||
final req = http.MultipartRequest('POST', Uri.parse('$_base/storage/upload'));
|
||||
if (token != null) req.headers['Authorization'] = 'Bearer $token';
|
||||
req.files.add(await http.MultipartFile.fromPath('file', file));
|
||||
final streamed = await req.send();
|
||||
final res = await http.Response.fromStream(streamed);
|
||||
final body = jsonDecode(res.body) as Map<String, dynamic>;
|
||||
final url = body['url'] as String;
|
||||
await _patch('/professionals/me', {'banner_picture': url});
|
||||
}
|
||||
|
||||
Future<String> uploadPdfCedula(String file, String userId) async {
|
||||
final token = await _getToken();
|
||||
final req = http.MultipartRequest('POST', Uri.parse('$_base/storage/upload'));
|
||||
if (token != null) req.headers['Authorization'] = 'Bearer $token';
|
||||
req.files.add(await http.MultipartFile.fromPath('file', file));
|
||||
final streamed = await req.send();
|
||||
final res = await http.Response.fromStream(streamed);
|
||||
final body = jsonDecode(res.body) as Map<String, dynamic>;
|
||||
return body['url'] as String;
|
||||
}
|
||||
|
||||
Future<String> uploadPdfCertificado(String file, String userId) async {
|
||||
final token = await _getToken();
|
||||
final req = http.MultipartRequest('POST', Uri.parse('$_base/storage/upload'));
|
||||
if (token != null) req.headers['Authorization'] = 'Bearer $token';
|
||||
req.files.add(await http.MultipartFile.fromPath('file', file));
|
||||
final streamed = await req.send();
|
||||
final res = await http.Response.fromStream(streamed);
|
||||
final body = jsonDecode(res.body) as Map<String, dynamic>;
|
||||
return body['url'] as String;
|
||||
}
|
||||
|
||||
Future<List<String>> uploadPdfsEspecializaciones(
|
||||
List<String> files, String userId) async {
|
||||
final List<String> urls = [];
|
||||
for (final file in files) {
|
||||
final token = await _getToken();
|
||||
final req = http.MultipartRequest('POST', Uri.parse('$_base/storage/upload'));
|
||||
if (token != null) req.headers['Authorization'] = 'Bearer $token';
|
||||
req.files.add(await http.MultipartFile.fromPath('file', file));
|
||||
final streamed = await req.send();
|
||||
final res = await http.Response.fromStream(streamed);
|
||||
final body = jsonDecode(res.body) as Map<String, dynamic>;
|
||||
urls.add(body['url'] as String);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
Future<List<ProfessionalEntity>> getProfessionalInfo() async {
|
||||
try {
|
||||
final data = await _get('/professionals') as List;
|
||||
return data.map((e) => _fromApi(e as Map<String, dynamic>)).toList();
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<ProfessionalEntity>> getProfessionalsFromIds(
|
||||
Iterable<String> ids) async {
|
||||
final result = <ProfessionalEntity>[];
|
||||
for (final id in ids) {
|
||||
final p = await getProInfo(id);
|
||||
if (p != null) result.add(p);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,14 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
equatable: ^2.0.5
|
||||
http: ^1.1.0
|
||||
shared_preferences: ^2.0.10
|
||||
intl: ^0.19.0
|
||||
|
||||
# Firebase
|
||||
# Firebase kept for FirebaseProfessionalRepository (legacy fallback)
|
||||
cloud_firestore: ^4.15.4
|
||||
firebase_storage: ^11.6.5
|
||||
firebase_core: ^2.25.4
|
||||
intl: ^0.19.0
|
||||
firebase_auth: ^4.17.8
|
||||
|
||||
dev_dependencies:
|
||||
@@ -25,4 +27,4 @@ dev_dependencies:
|
||||
sdk: flutter
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
uses-material-design: true
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
library chat_repository;
|
||||
library score_repository;
|
||||
|
||||
export 'src/entities/entities.dart';
|
||||
export 'src/repositories/firebase_score_repository.dart';
|
||||
export 'src/repositories/api_score_repository.dart';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class CommentEntity extends Equatable {
|
||||
@@ -8,7 +7,7 @@ class CommentEntity extends Equatable {
|
||||
final String content;
|
||||
final double score;
|
||||
final bool isFromUser;
|
||||
final Timestamp createdAt;
|
||||
final String createdAt;
|
||||
|
||||
const CommentEntity({
|
||||
required this.authorId,
|
||||
@@ -28,7 +27,7 @@ class CommentEntity extends Equatable {
|
||||
content: doc['content'] as String,
|
||||
score: doc['score'] as double,
|
||||
isFromUser: doc['is_from_user'] as bool,
|
||||
createdAt: doc['created_at'] as Timestamp,
|
||||
createdAt: doc['created_at']?.toString() ?? DateTime.now().toIso8601String(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class ReputationEntity extends Equatable {
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:score_repository/score_repository.dart';
|
||||
import 'package:score_repository/src/entities/comment_entity.dart';
|
||||
|
||||
const _base = 'https://backend.prosapp.co/api/v1';
|
||||
|
||||
/// API-backed replacement for FirebaseScoreRepository.
|
||||
/// Mirrors the same public API so existing blocs work without changes.
|
||||
class ApiScoreRepository {
|
||||
ReputationEntity? _reputation;
|
||||
final StreamController<ReputationEntity> _reputationController =
|
||||
StreamController<ReputationEntity>.broadcast();
|
||||
|
||||
String? _token;
|
||||
|
||||
ApiScoreRepository() {
|
||||
_reputationController.add(_emptyReputation());
|
||||
}
|
||||
|
||||
ReputationEntity _emptyReputation() => const ReputationEntity(
|
||||
total: 0,
|
||||
average: 0,
|
||||
totalPro: 0,
|
||||
averagePro: 0,
|
||||
);
|
||||
|
||||
Future<String?> _getToken() async {
|
||||
if (_token != null) return _token;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return _token = prefs.getString('token');
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _headers() async {
|
||||
final t = await _getToken();
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
if (t != null) 'Authorization': 'Bearer $t',
|
||||
};
|
||||
}
|
||||
|
||||
Stream<ReputationEntity> streamReputation() => _reputationController.stream;
|
||||
|
||||
ReputationEntity getReputation() => _reputation ?? _emptyReputation();
|
||||
|
||||
Future<ReputationEntity> getReputationByUserId(String userId) async {
|
||||
try {
|
||||
final res = await http.get(
|
||||
Uri.parse('$_base/comments/reputation/$userId'),
|
||||
headers: await _headers(),
|
||||
);
|
||||
final data = jsonDecode(res.body) as Map<String, dynamic>;
|
||||
final rep = ReputationEntity.fromDocument(data);
|
||||
_reputation = rep;
|
||||
_reputationController.add(rep);
|
||||
return rep;
|
||||
} catch (_) {
|
||||
return _emptyReputation();
|
||||
}
|
||||
}
|
||||
|
||||
Stream<List<CommentEntity>> getScoresForUser(String userId) {
|
||||
final controller = StreamController<List<CommentEntity>>();
|
||||
_fetchComments(userId: userId, isFromUser: false).then((list) {
|
||||
controller.add(list);
|
||||
controller.close();
|
||||
}).catchError((e) {
|
||||
controller.add([]);
|
||||
controller.close();
|
||||
});
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
Stream<List<CommentEntity>> getScoresForProfessional(String userId) {
|
||||
final controller = StreamController<List<CommentEntity>>();
|
||||
_fetchComments(userId: userId, isFromUser: true).then((list) {
|
||||
controller.add(list);
|
||||
controller.close();
|
||||
}).catchError((e) {
|
||||
controller.add([]);
|
||||
controller.close();
|
||||
});
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
Future<List<CommentEntity>> _fetchComments({
|
||||
required String userId,
|
||||
required bool isFromUser,
|
||||
}) async {
|
||||
try {
|
||||
final res = await http.get(
|
||||
Uri.parse('$_base/comments/reputation/$userId'),
|
||||
headers: await _headers(),
|
||||
);
|
||||
final data = jsonDecode(res.body);
|
||||
if (data is! Map) return [];
|
||||
// The endpoint returns reputation summary, not individual comments
|
||||
// Return empty list since we only have aggregated data
|
||||
return [];
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> addComment(CommentEntity comment) async {
|
||||
try {
|
||||
await http.post(
|
||||
Uri.parse('$_base/comments'),
|
||||
headers: await _headers(),
|
||||
body: jsonEncode({
|
||||
'author_id': comment.authorId,
|
||||
'destination_id': comment.destinationId,
|
||||
'service_id': comment.serviceId,
|
||||
'content': comment.content,
|
||||
'score': comment.score,
|
||||
'is_from_user': comment.isFromUser,
|
||||
}),
|
||||
);
|
||||
// Refresh reputation after adding a comment
|
||||
await getReputationByUserId(comment.destinationId);
|
||||
} catch (_) {
|
||||
// Stub — do not crash if comment fails
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,10 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
equatable: ^2.0.5
|
||||
http: ^1.1.0
|
||||
shared_preferences: ^2.0.10
|
||||
|
||||
# Firebase
|
||||
# Firebase kept for FirebaseScoreRepository (legacy) and CommentEntity uses Timestamp
|
||||
cloud_firestore: ^4.15.4
|
||||
firebase_core: ^2.25.4
|
||||
firebase_auth: ^4.17.4
|
||||
@@ -23,4 +25,4 @@ dev_dependencies:
|
||||
sdk: flutter
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
uses-material-design: true
|
||||
|
||||
@@ -3,3 +3,4 @@ library service_repository;
|
||||
export 'src/entities/entities.dart';
|
||||
export 'src/models/models.dart';
|
||||
export 'src/repositories/firebase_service_repository.dart';
|
||||
export 'src/repositories/api_service_repository.dart';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:service_repository/service_repository.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -14,7 +13,7 @@ class ServiceEntity extends Equatable {
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
final String day;
|
||||
final Timestamp createdAt;
|
||||
final String createdAt;
|
||||
final String description;
|
||||
final TimeOfDay range1Hour1;
|
||||
final TimeOfDay range1Hour2;
|
||||
@@ -54,7 +53,7 @@ class ServiceEntity extends Equatable {
|
||||
latitude: doc['latitude'] as double,
|
||||
longitude: doc['longitude'] as double,
|
||||
day: doc['day'] as String,
|
||||
createdAt: doc['created_at'] as Timestamp,
|
||||
createdAt: doc['created_at']?.toString() ?? DateTime.now().toIso8601String(),
|
||||
description: doc['description'] as String,
|
||||
range1Hour1: parseTimeOfDay(doc['range1_hour1'] as String),
|
||||
range1Hour2: parseTimeOfDay(doc['range1_hour2'] as String),
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:service_repository/service_repository.dart';
|
||||
|
||||
const _base = 'https://backend.prosapp.co/api/v1';
|
||||
|
||||
/// API-backed replacement for FirebaseServiceRepository.
|
||||
/// Mirrors the same public API so existing blocs work without changes.
|
||||
class ApiServiceRepository {
|
||||
String? _token;
|
||||
|
||||
Future<String?> _getToken() async {
|
||||
if (_token != null) return _token;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return _token = prefs.getString('token');
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _headers() async {
|
||||
final t = await _getToken();
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
if (t != null) 'Authorization': 'Bearer $t',
|
||||
};
|
||||
}
|
||||
|
||||
Future<dynamic> _get(String path, {Map<String, String>? query}) async {
|
||||
final uri = Uri.parse('$_base$path').replace(queryParameters: query);
|
||||
final res = await http.get(uri, headers: await _headers());
|
||||
return jsonDecode(res.body);
|
||||
}
|
||||
|
||||
Future<dynamic> _post(String path, Map<String, dynamic> body) async {
|
||||
final res = await http.post(
|
||||
Uri.parse('$_base$path'),
|
||||
headers: await _headers(),
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
return jsonDecode(res.body);
|
||||
}
|
||||
|
||||
Future<dynamic> _patch(String path, Map<String, dynamic> body) async {
|
||||
final res = await http.patch(
|
||||
Uri.parse('$_base$path'),
|
||||
headers: await _headers(),
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
return jsonDecode(res.body);
|
||||
}
|
||||
|
||||
ServiceEntity _fromApi(Map<String, dynamic> json) {
|
||||
String range1Hour1 = json['range1_hour1']?.toString() ?? '0:0';
|
||||
String range1Hour2 = json['range1_hour2']?.toString() ?? '0:0';
|
||||
|
||||
TimeOfDay parseTime(String s) {
|
||||
final parts = s.split(':');
|
||||
return TimeOfDay(
|
||||
hour: int.tryParse(parts[0]) ?? 0,
|
||||
minute: int.tryParse(parts.length > 1 ? parts[1] : '0') ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
return ServiceEntity(
|
||||
id: json['id']?.toString(),
|
||||
professionalId: json['professional_id']?.toString() ?? '',
|
||||
professionalScored: json['professional_scored'] as bool? ?? false,
|
||||
userId: json['user_id']?.toString() ?? '',
|
||||
userScored: json['user_scored'] as bool? ?? false,
|
||||
address: json['address']?.toString() ?? '',
|
||||
aditionalAddress: json['aditional_address']?.toString() ?? '',
|
||||
latitude: double.tryParse(json['latitude']?.toString() ?? '0') ?? 0.0,
|
||||
longitude: double.tryParse(json['longitude']?.toString() ?? '0') ?? 0.0,
|
||||
day: json['day']?.toString() ?? '',
|
||||
createdAt: json['created_at']?.toString() ?? DateTime.now().toIso8601String(),
|
||||
description: json['description']?.toString() ?? '',
|
||||
range1Hour1: parseTime(range1Hour1),
|
||||
range1Hour2: parseTime(range1Hour2),
|
||||
rate: json['rate']?.toString() ?? '',
|
||||
status: intToEnumService((json['status'] as num?)?.toInt() ?? 0),
|
||||
location: intToEnum((json['location'] as num?)?.toInt() ?? 0),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> createService(ServiceEntity entity) async {
|
||||
final data = await _post('/services', entity.toDocument());
|
||||
return data['id']?.toString() ?? '';
|
||||
}
|
||||
|
||||
Future<void> updateServiceStatus(String serviceId, ServiceStatus newStatus) async {
|
||||
await _patch('/services/$serviceId', {'status': enumToIntService(newStatus)});
|
||||
}
|
||||
|
||||
Stream<ServiceEntity> getService(String serviceId) {
|
||||
final controller = StreamController<ServiceEntity>();
|
||||
_get('/services', query: {'id': serviceId}).then((data) {
|
||||
if (data is List && data.isNotEmpty) {
|
||||
controller.add(_fromApi(data.first as Map<String, dynamic>));
|
||||
} else if (data is Map) {
|
||||
controller.add(_fromApi(data as Map<String, dynamic>));
|
||||
}
|
||||
controller.close();
|
||||
}).catchError((e) {
|
||||
controller.addError(e);
|
||||
controller.close();
|
||||
});
|
||||
return controller.stream;
|
||||
}
|
||||
|
||||
Stream<List<ServiceEntity>> getServicesForUser(String userId) {
|
||||
return _streamList('/services', query: {'userId': userId}, statusFilter: [0, 1, 3]);
|
||||
}
|
||||
|
||||
Stream<List<ServiceEntity>> getServicesForProfessional(String professionalId) {
|
||||
return _streamList('/services', query: {'professionalId': professionalId}, statusFilter: [1, 3]);
|
||||
}
|
||||
|
||||
Future<List<ServiceEntity>> getServicesForProfessionalforCalendar(String professionalId) async {
|
||||
try {
|
||||
final data = await _get('/services', query: {'professionalId': professionalId});
|
||||
if (data is! List) return [];
|
||||
return data
|
||||
.map((e) => _fromApi(e as Map<String, dynamic>))
|
||||
.where((s) => [0, 1, 2, 3, 6].contains(s.status.index))
|
||||
.toList();
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Stream<List<ServiceEntity>> getServicesHistoryForUser(String userId) {
|
||||
return _streamList('/services', query: {'userId': userId}, statusFilter: [2, 4, 5]);
|
||||
}
|
||||
|
||||
Stream<List<ServiceEntity>> getServicesHistoryForProfessional(String professionalId) {
|
||||
return _streamList('/services', query: {'professionalId': professionalId}, statusFilter: [2, 4, 5]);
|
||||
}
|
||||
|
||||
Stream<List<ServiceEntity>> getPendingServicesForProfessional(String professionalId) {
|
||||
return _streamList('/services', query: {'professionalId': professionalId}, statusFilter: [0]);
|
||||
}
|
||||
|
||||
Future<int> countPendingServicesForProfessional(String professionalId) async {
|
||||
try {
|
||||
final data = await _get('/services', query: {'professionalId': professionalId});
|
||||
if (data is! List) return 0;
|
||||
return data
|
||||
.map((e) => _fromApi(e as Map<String, dynamic>))
|
||||
.where((s) => s.status == ServiceStatus.pending)
|
||||
.length;
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setProfessionalScored(String serviceId) async {
|
||||
await _patch('/services/$serviceId', {'professional_scored': true});
|
||||
}
|
||||
|
||||
Future<void> setUserScored(String serviceId) async {
|
||||
await _patch('/services/$serviceId', {'user_scored': true});
|
||||
}
|
||||
|
||||
Stream<List<ServiceEntity>> _streamList(
|
||||
String path, {
|
||||
Map<String, String>? query,
|
||||
List<int> statusFilter = const [],
|
||||
}) {
|
||||
final controller = StreamController<List<ServiceEntity>>();
|
||||
_get(path, query: query).then((data) {
|
||||
if (data is! List) {
|
||||
controller.add([]);
|
||||
} else {
|
||||
var list = data.map((e) => _fromApi(e as Map<String, dynamic>)).toList();
|
||||
if (statusFilter.isNotEmpty) {
|
||||
list = list.where((s) => statusFilter.contains(s.status.index)).toList();
|
||||
}
|
||||
controller.add(list);
|
||||
}
|
||||
controller.close();
|
||||
}).catchError((e) {
|
||||
controller.add([]);
|
||||
controller.close();
|
||||
});
|
||||
return controller.stream;
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,10 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
equatable: ^2.0.5
|
||||
http: ^1.1.0
|
||||
shared_preferences: ^2.0.10
|
||||
|
||||
# Firebase
|
||||
# Firebase kept for FirebaseServiceRepository (legacy) and ServiceEntity uses Timestamp
|
||||
cloud_firestore: ^4.15.4
|
||||
firebase_core: ^2.25.4
|
||||
|
||||
@@ -22,4 +24,4 @@ dev_dependencies:
|
||||
sdk: flutter
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
uses-material-design: true
|
||||
|
||||
@@ -3,3 +3,4 @@ library setting_repository;
|
||||
export 'src/entities/entities.dart';
|
||||
export 'src/repositories/setting_repo.dart';
|
||||
export 'src/repositories/firebase_setting_repository.dart';
|
||||
export 'src/repositories/api_setting_repository.dart';
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:setting_repository/src/entities/entities.dart';
|
||||
import 'package:setting_repository/src/repositories/setting_repo.dart';
|
||||
|
||||
const _base = 'https://backend.prosapp.co/api/v1';
|
||||
|
||||
class ApiSettingRepository implements SettingRepository {
|
||||
Future<String?> _getToken() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString('token');
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _headers() async {
|
||||
final t = await _getToken();
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
if (t != null) 'Authorization': 'Bearer $t',
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SettingEntity> getSettings() async {
|
||||
try {
|
||||
final res = await http.get(
|
||||
Uri.parse('$_base/settings'),
|
||||
headers: await _headers(),
|
||||
);
|
||||
final data = jsonDecode(res.body);
|
||||
|
||||
// Backend returns {key: string, value: any}[] or a flat map
|
||||
Map<String, dynamic> doc = {};
|
||||
if (data is List) {
|
||||
for (final entry in data) {
|
||||
if (entry is Map) {
|
||||
final key = entry['key']?.toString();
|
||||
final value = entry['value'];
|
||||
if (key != null) doc[key] = value;
|
||||
}
|
||||
}
|
||||
} else if (data is Map) {
|
||||
doc = Map<String, dynamic>.from(data);
|
||||
}
|
||||
|
||||
return SettingEntity.fromDocument(doc);
|
||||
} catch (_) {
|
||||
// Return safe defaults if settings call fails
|
||||
return const SettingEntity(
|
||||
tarifas: null,
|
||||
domicilios: null,
|
||||
google: null,
|
||||
versionIos: null,
|
||||
versionAndroid: null,
|
||||
horaNotificacion: null,
|
||||
tituloSoporte: null,
|
||||
parrafoSoporte: null,
|
||||
numeroSoporte: null,
|
||||
emailSoporte: null,
|
||||
diasSoporte: null,
|
||||
horasSoporte: null,
|
||||
politicasPrivacidad: null,
|
||||
politicasPrivacidadTitle: null,
|
||||
politicasPrivacidadBody: null,
|
||||
terminosCondiciones: null,
|
||||
terminosCondicionesTitle: null,
|
||||
terminosCondicionesBody: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,10 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
equatable: ^2.0.5
|
||||
http: ^1.1.0
|
||||
shared_preferences: ^2.0.10
|
||||
|
||||
# Firebase
|
||||
# Firebase kept for FirebaseSettingRepository (legacy fallback)
|
||||
cloud_firestore: ^4.15.4
|
||||
firebase_core: ^2.25.4
|
||||
|
||||
@@ -23,4 +25,4 @@ dev_dependencies:
|
||||
sdk: flutter
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
uses-material-design: true
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/models.dart';
|
||||
import 'user_repo.dart';
|
||||
|
||||
const _base = 'https://backend.prosapp.co/api/v1';
|
||||
|
||||
class ApiUserRepository implements UserRepository {
|
||||
static ApiUserRepository? _instance;
|
||||
|
||||
// Cached user ID accessible without async for UI usage
|
||||
static String? currentUserId;
|
||||
|
||||
final _controller = StreamController<MyUser?>.broadcast();
|
||||
MyUser? _current;
|
||||
String? _token;
|
||||
|
||||
Future<String?> _getToken() async {
|
||||
if (_token != null) return _token;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return _token = prefs.getString('token');
|
||||
}
|
||||
|
||||
Future<Map<String, String>> _headers() async {
|
||||
final t = await _getToken();
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
if (t != null) 'Authorization': 'Bearer $t',
|
||||
};
|
||||
}
|
||||
|
||||
Future<dynamic> _get(String path) async {
|
||||
final res = await http.get(Uri.parse('$_base$path'), headers: await _headers());
|
||||
return jsonDecode(res.body);
|
||||
}
|
||||
|
||||
Future<dynamic> _post(String path, Map<String, dynamic> body, {bool auth = false}) async {
|
||||
final h = await _headers();
|
||||
if (!auth) h.remove('Authorization');
|
||||
final res = await http.post(Uri.parse('$_base$path'), headers: h, body: jsonEncode(body));
|
||||
return jsonDecode(res.body);
|
||||
}
|
||||
|
||||
Future<dynamic> _patch(String path, Map<String, dynamic> body) async {
|
||||
final res = await http.patch(Uri.parse('$_base$path'), headers: await _headers(), body: jsonEncode(body));
|
||||
return jsonDecode(res.body);
|
||||
}
|
||||
|
||||
MyUser _fromApi(Map<String, dynamic> json) {
|
||||
return MyUser(
|
||||
id: json['id']?.toString() ?? '',
|
||||
email: json['email']?.toString(),
|
||||
phone: json['phone']?.toString(),
|
||||
name: json['name']?.toString(),
|
||||
nickname: json['nickname']?.toString(),
|
||||
city: json['city']?.toString(),
|
||||
picture: json['picture']?.toString(),
|
||||
birthday: json['birthday']?.toString(),
|
||||
gender: json['gender']?.toString(),
|
||||
proState: _proStateFromInt((json['pro_state'] as num?)?.toInt() ?? 0),
|
||||
token: null,
|
||||
);
|
||||
}
|
||||
|
||||
ProState _proStateFromInt(int v) {
|
||||
switch (v) {
|
||||
case 1:
|
||||
return ProState.pending;
|
||||
case 2:
|
||||
return ProState.active;
|
||||
case 3:
|
||||
return ProState.denied;
|
||||
default:
|
||||
return ProState.inactive;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MyUser?> lastUser() async => _current;
|
||||
|
||||
@override
|
||||
Stream<MyUser?> streamUser() => _controller.stream;
|
||||
|
||||
@override
|
||||
Stream<bool> isAuthenticated() async* {
|
||||
final t = await _getToken();
|
||||
if (t != null) {
|
||||
// Try to restore the current user from the API
|
||||
try {
|
||||
final data = await _get('/auth/me');
|
||||
if (data != null) {
|
||||
_current = _fromApi(data as Map<String, dynamic>);
|
||||
currentUserId = _current?.id;
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
yield t != null;
|
||||
}
|
||||
|
||||
void _emit(MyUser? user) {
|
||||
currentUserId = user?.id;
|
||||
_controller.add(user);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> signIn(String email, String password) async {
|
||||
final data = await _post('/auth/login', {'email': email, 'password': password});
|
||||
_token = data['access_token'] as String?;
|
||||
if (_token != null) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('token', _token!);
|
||||
}
|
||||
_current = _fromApi(data['user'] as Map<String, dynamic>);
|
||||
_emit(_current);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MyUser> signUp(MyUser myUser, String password) async {
|
||||
final data = await _post('/auth/register', {
|
||||
'email': myUser.email ?? '',
|
||||
'password': password,
|
||||
'name': myUser.name ?? myUser.email ?? '',
|
||||
});
|
||||
_token = data['access_token'] as String?;
|
||||
if (_token != null) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('token', _token!);
|
||||
}
|
||||
_current = _fromApi(data['user'] as Map<String, dynamic>);
|
||||
_emit(_current);
|
||||
return _current!;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> logOut() async {
|
||||
_token = null;
|
||||
_current = null;
|
||||
currentUserId = null;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove('token');
|
||||
_controller.add(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MyUser?> getMyUser(String myUserId) async {
|
||||
try {
|
||||
final data = await _get('/users/$myUserId');
|
||||
return _fromApi(data as Map<String, dynamic>);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateUserInfo(MyUser myUser) async {
|
||||
final body = <String, dynamic>{};
|
||||
if (myUser.name != null) body['name'] = myUser.name;
|
||||
if (myUser.city != null) body['city'] = myUser.city;
|
||||
if (myUser.picture != null) body['picture'] = myUser.picture;
|
||||
if (myUser.birthday != null) body['birthday'] = myUser.birthday;
|
||||
if (myUser.gender != null) body['gender'] = myUser.gender;
|
||||
if (myUser.phone != null) body['phone'] = myUser.phone;
|
||||
if (body.isNotEmpty) {
|
||||
await _patch('/users/me', body);
|
||||
}
|
||||
_current = _current?.copyWith(
|
||||
name: myUser.name,
|
||||
city: myUser.city,
|
||||
picture: myUser.picture,
|
||||
birthday: myUser.birthday,
|
||||
gender: myUser.gender,
|
||||
phone: myUser.phone,
|
||||
);
|
||||
_emit(_current);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setUserData(MyUser user) => updateUserInfo(user);
|
||||
|
||||
@override
|
||||
Future<void> createUser(MyUser myUser) async {
|
||||
// Called after signUp; user already created in backend
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> uploadPicture(String file, String userId) async {
|
||||
final token = await _getToken();
|
||||
final req = http.MultipartRequest('POST', Uri.parse('$_base/storage/upload'));
|
||||
if (token != null) req.headers['Authorization'] = 'Bearer $token';
|
||||
req.files.add(await http.MultipartFile.fromPath('file', file));
|
||||
final streamed = await req.send();
|
||||
final res = await http.Response.fromStream(streamed);
|
||||
final body = jsonDecode(res.body) as Map<String, dynamic>;
|
||||
final url = body['url'] as String;
|
||||
await _patch('/users/me', {'picture': url});
|
||||
return url;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MyUser>> getUsersProfessionalActive() async {
|
||||
try {
|
||||
final data = await _get('/professionals') as List;
|
||||
final List<MyUser> result = [];
|
||||
for (final p in data) {
|
||||
final userId = p['user_id']?.toString();
|
||||
if (userId != null) {
|
||||
final user = await getMyUser(userId);
|
||||
if (user != null) result.add(user);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MyUser>> getUsersFromIds(Iterable<String> ids) async {
|
||||
final result = <MyUser>[];
|
||||
for (final id in ids) {
|
||||
final u = await getMyUser(id);
|
||||
if (u != null) result.add(u);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> addEmailAndPassword(String email, String password) async {
|
||||
try {
|
||||
await _patch('/users/me', {'email': email});
|
||||
return null;
|
||||
} catch (e) {
|
||||
return e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<UpdatePassworErros?> updatePassword(String password, String newPassword) async {
|
||||
// Backend doesn't expose a change-password endpoint with old password; stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> signInWithPhoneNumber(String phoneNumber) async {
|
||||
// The API's phone auth is direct — no OTP flow via this method
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> verifyOTP(String code) async => false;
|
||||
|
||||
@override
|
||||
Future<void> addPhoneAuthCredential(
|
||||
String password,
|
||||
String phoneNumber, {
|
||||
required Future<void> Function(Exception) verificationFailed,
|
||||
required Future<void> Function(String) codeSent,
|
||||
required Future<void> Function(String) codeAutoRetrievalTimeout,
|
||||
}) async {
|
||||
// Not supported by new REST API — stub
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> linkWithOTP(String phoneNumber, String verificationId, String code) async => false;
|
||||
|
||||
@override
|
||||
Future<void> resetPassword(String email) async {
|
||||
// Not supported by current API — stub
|
||||
}
|
||||
}
|
||||
@@ -213,7 +213,7 @@ class FirebaseUserRepository implements UserRepository {
|
||||
|
||||
@override
|
||||
addPhoneAuthCredential(String password, String phoneNumber,
|
||||
{required Future<void> Function(FirebaseAuthException) verificationFailed,
|
||||
{required Future<void> Function(Exception) verificationFailed,
|
||||
required Future<void> Function(String) codeSent,
|
||||
required Future<void> Function(String) codeAutoRetrievalTimeout}) async {
|
||||
await _firebaseAuth.verifyPhoneNumber(
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
|
||||
import '../../user_repository.dart';
|
||||
|
||||
abstract class UserRepository {
|
||||
@@ -25,7 +23,7 @@ abstract class UserRepository {
|
||||
Future<bool> verifyOTP(String code);
|
||||
|
||||
Future<void> addPhoneAuthCredential(String password, String phoneNumber,
|
||||
{required Future<void> Function(FirebaseAuthException) verificationFailed,
|
||||
{required Future<void> Function(Exception) verificationFailed,
|
||||
required Future<void> Function(String) codeSent,
|
||||
required Future<void> Function(String) codeAutoRetrievalTimeout});
|
||||
|
||||
|
||||
@@ -1,43 +1,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
|
||||
class PhoneVerificationService {
|
||||
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
|
||||
|
||||
Stream<PhoneAuthEvent> verifyPhoneNumber(String phoneNumber) async* {
|
||||
final StreamController<PhoneAuthEvent> phoneAuthController =
|
||||
StreamController<PhoneAuthEvent>();
|
||||
|
||||
_firebaseAuth.verifyPhoneNumber(
|
||||
phoneNumber: phoneNumber,
|
||||
timeout: const Duration(seconds: 60),
|
||||
verificationCompleted: (AuthCredential authCredential) async {
|
||||
phoneAuthController
|
||||
.add(PhoneAuthEvent.verificationCompleted(authCredential));
|
||||
},
|
||||
verificationFailed: (FirebaseAuthException authException) async {
|
||||
phoneAuthController
|
||||
.add(PhoneAuthEvent.verificationFailed(authException));
|
||||
phoneAuthController.close();
|
||||
},
|
||||
codeAutoRetrievalTimeout: (String verificationId) async {
|
||||
phoneAuthController
|
||||
.add(PhoneAuthEvent.codeAutoRetrievalTimeout(verificationId));
|
||||
},
|
||||
codeSent: (String verificationId, int? resendToken) async {
|
||||
phoneAuthController
|
||||
.add(PhoneAuthEvent.codeSent(verificationId, resendToken));
|
||||
},
|
||||
);
|
||||
|
||||
await for (PhoneAuthEvent event in phoneAuthController.stream) {
|
||||
yield event;
|
||||
if (event.type == PhoneAuthEventType.verificationFailed) {
|
||||
await phoneAuthController.close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// ponytail: Firebase phone auth removed — stub until backend OTP is implemented
|
||||
yield PhoneAuthEvent(PhoneAuthEventType.verificationFailed, Exception('Phone verification not supported'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,23 +20,16 @@ class PhoneAuthEvent {
|
||||
|
||||
PhoneAuthEvent(this.type, this.data);
|
||||
|
||||
static PhoneAuthEvent verificationCompleted(AuthCredential authCredential) {
|
||||
return PhoneAuthEvent(
|
||||
PhoneAuthEventType.verificationCompleted, authCredential);
|
||||
}
|
||||
static PhoneAuthEvent verificationCompleted(dynamic credential) =>
|
||||
PhoneAuthEvent(PhoneAuthEventType.verificationCompleted, credential);
|
||||
|
||||
static PhoneAuthEvent verificationFailed(
|
||||
FirebaseAuthException authException) {
|
||||
return PhoneAuthEvent(PhoneAuthEventType.verificationFailed, authException);
|
||||
}
|
||||
static PhoneAuthEvent verificationFailed(Exception e) =>
|
||||
PhoneAuthEvent(PhoneAuthEventType.verificationFailed, e);
|
||||
|
||||
static PhoneAuthEvent codeAutoRetrievalTimeout(String verificationId) {
|
||||
return PhoneAuthEvent(
|
||||
PhoneAuthEventType.codeAutoRetrievalTimeout, verificationId);
|
||||
}
|
||||
static PhoneAuthEvent codeAutoRetrievalTimeout(String verificationId) =>
|
||||
PhoneAuthEvent(PhoneAuthEventType.codeAutoRetrievalTimeout, verificationId);
|
||||
|
||||
static PhoneAuthEvent codeSent(String verificationId, int? resendToken) {
|
||||
return PhoneAuthEvent(PhoneAuthEventType.codeSent,
|
||||
{'verificationId': verificationId, 'resendToken': resendToken});
|
||||
}
|
||||
static PhoneAuthEvent codeSent(String verificationId, int? resendToken) =>
|
||||
PhoneAuthEvent(PhoneAuthEventType.codeSent,
|
||||
{'verificationId': verificationId, 'resendToken': resendToken});
|
||||
}
|
||||
|
||||
@@ -5,3 +5,4 @@ export 'src/entities/entities.dart';
|
||||
export 'src/services/phone_verification_service.dart';
|
||||
export 'src/repositories/user_repo.dart';
|
||||
export 'src/repositories/firebase_user_repository.dart';
|
||||
export 'src/repositories/api_user_repository.dart';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: user_repository
|
||||
description: Dart package which manages the user.
|
||||
publish_to: "none"
|
||||
publish_to: "none"
|
||||
|
||||
version: 1.0.11+11
|
||||
|
||||
@@ -11,8 +11,10 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
equatable: ^2.0.5
|
||||
http: ^1.1.0
|
||||
shared_preferences: ^2.0.10
|
||||
|
||||
# Firebase
|
||||
# Firebase kept for FirebaseUserRepository (legacy fallback)
|
||||
firebase_auth: ^4.17.4
|
||||
cloud_firestore: ^4.15.4
|
||||
firebase_storage: ^11.6.5
|
||||
|
||||
Reference in New Issue
Block a user