MotorSignal-App/lib/services/social/rider_profile_service.dart

222 lines
7.6 KiB
Dart

import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'handle_validator.dart';
import 'rider_profile_models.dart';
/// Service managing public rider profile persistence, unique handle reservations,
/// and privacy preferences in Cloud Firestore.
class RiderProfileService {
final FirebaseFirestore? _firestore;
RiderProfileService({FirebaseFirestore? firestore})
: _firestore = firestore ?? _resolveFirestore();
static FirebaseFirestore? _resolveFirestore() {
try {
if (Firebase.apps.isNotEmpty) return FirebaseFirestore.instance;
} catch (_) {}
return null;
}
bool get isAvailable => _firestore != null;
/// Fetches a rider's profile by UID (reads owner profile, falls back to public profile).
Future<RiderProfile?> getProfile(String uid) async {
final firestore = _firestore;
if (firestore == null) return null;
try {
final doc = await firestore.collection('users').doc(uid).get();
if (doc.exists && doc.data() != null) {
return RiderProfile.fromJson(doc.data()!);
}
return await getPublicProfile(uid);
} catch (e) {
// If reading private user doc fails (e.g. PERMISSION_DENIED for other user), read public projection
return await getPublicProfile(uid);
}
}
/// Fetches the public projection of a rider's profile.
Future<RiderProfile?> getPublicProfile(String uid) async {
final firestore = _firestore;
if (firestore == null) return null;
try {
final doc = await firestore.collection('public_profiles').doc(uid).get();
if (!doc.exists || doc.data() == null) return null;
return RiderProfile.fromPublicProfileJson(doc.data()!);
} catch (e) {
debugPrint('[RiderProfileService] Failed to load public profile for $uid: $e');
return null;
}
}
/// Realtime stream of a rider's profile by UID.
Stream<RiderProfile?> profileStream(String uid) {
final firestore = _firestore;
if (firestore == null) return Stream.value(null);
return firestore
.collection('users')
.doc(uid)
.snapshots()
.map((snapshot) {
if (!snapshot.exists || snapshot.data() == null) return null;
try {
return RiderProfile.fromJson(snapshot.data()!);
} catch (e) {
debugPrint('[RiderProfileService] Stream parse error for $uid: $e');
return null;
}
})
.handleError((e) {
debugPrint('[RiderProfileService] Stream error for $uid: $e');
return null;
});
}
/// Checks if a normalized handle is available for reservation.
/// If [currentUid] is provided, returning true if the handle is already owned by [currentUid].
Future<bool> isHandleAvailable(String handle, {String? currentUid}) async {
final validation = HandleValidator.validate(handle);
if (!validation.isValid) return false;
final firestore = _firestore;
if (firestore == null) return true;
final normalized = validation.normalizedHandle!;
final doc = await firestore.collection('handles').doc(normalized).get();
if (!doc.exists) return true;
final data = doc.data();
final ownerUid = data?['uid'] as String?;
return currentUid != null && ownerUid == currentUid;
}
/// Saves or updates a rider's profile while atomically securing the handle reservation.
Future<RiderProfile> saveProfile({
required String uid,
required String handle,
required String displayName,
String bio = '',
String region = '',
String? avatarPreset,
String? avatarUrl,
ProfilePrivacySettings? privacySettings,
}) async {
final validation = HandleValidator.validate(handle);
if (!validation.isValid) {
throw ArgumentError(validation.error);
}
final normalizedHandle = validation.normalizedHandle!;
final firestore = _firestore;
final now = DateTime.now();
if (firestore == null) {
// Offline/local fallback
return RiderProfile(
uid: uid,
handle: normalizedHandle,
displayName: displayName.trim(),
bio: bio.trim(),
region: region.trim(),
avatarPreset: avatarPreset,
avatarUrl: avatarUrl,
privacySettings: privacySettings ?? const ProfilePrivacySettings(),
createdAt: now,
updatedAt: now,
);
}
final userRef = firestore.collection('users').doc(uid);
final publicRef = firestore.collection('public_profiles').doc(uid);
final handleRef = firestore.collection('handles').doc(normalizedHandle);
// Read current user profile to determine previous handle if changing
final currentDoc = await userRef.get();
final existingProfile =
currentDoc.exists && currentDoc.data() != null
? RiderProfile.fromJson(currentDoc.data()!)
: null;
final previousHandle = existingProfile?.handle;
final isChangingHandle =
previousHandle != null && previousHandle.isNotEmpty && previousHandle != normalizedHandle;
final updatedProfile = RiderProfile(
uid: uid,
handle: normalizedHandle,
displayName: displayName.trim().isEmpty ? normalizedHandle : displayName.trim(),
bio: bio.trim(),
region: region.trim(),
avatarPreset: avatarPreset,
avatarUrl: avatarUrl,
privacySettings: privacySettings ?? existingProfile?.privacySettings ?? const ProfilePrivacySettings(),
createdAt: existingProfile?.createdAt ?? now,
updatedAt: now,
);
await firestore.runTransaction((transaction) async {
// 1. Verify handle availability
final handleSnapshot = await transaction.get(handleRef);
if (handleSnapshot.exists) {
final ownerUid = handleSnapshot.data()?['uid'] as String?;
if (ownerUid != uid) {
throw StateError('Handle @$normalizedHandle is already taken.');
}
}
// 2. Reserve new handle (only if not already owned by us, to avoid violating update: if false rules)
if (!handleSnapshot.exists) {
transaction.set(handleRef, {
'handle': normalizedHandle,
'uid': uid,
'createdAt': FieldValue.serverTimestamp(),
});
}
// 3. Release previous handle if changed
if (isChangingHandle && previousHandle.isNotEmpty) {
final oldHandleRef = firestore.collection('handles').doc(previousHandle);
transaction.delete(oldHandleRef);
}
// 4. Save private user profile document
transaction.set(userRef, updatedProfile.toJson(), SetOptions(merge: true));
// 5. Save public profile projection
transaction.set(publicRef, updatedProfile.toPublicProfileJson(), SetOptions(merge: true));
});
final saved = await getProfile(uid);
return saved ?? updatedProfile;
}
/// Releases the rider's handle and cleans up the profile doc.
Future<void> deleteProfile(String uid) async {
final firestore = _firestore;
if (firestore == null) return;
try {
final userRef = firestore.collection('users').doc(uid);
final publicRef = firestore.collection('public_profiles').doc(uid);
final doc = await userRef.get();
if (doc.exists && doc.data() != null) {
final handle = doc.data()?['username'] as String?;
if (handle != null && handle.isNotEmpty) {
await firestore.collection('handles').doc(handle.toLowerCase()).delete();
}
}
await userRef.delete();
await publicRef.delete();
} catch (e) {
debugPrint('[RiderProfileService] Failed to delete profile for $uid: $e');
rethrow;
}
}
}