Merge dev into feature branch

This commit is contained in:
2026-03-19 13:16:04 +05:30
273 changed files with 7867 additions and 3654 deletions

View File

@@ -33,7 +33,10 @@ class ClientAuthenticationModule extends Module {
void binds(Injector i) {
// Repositories
i.addLazySingleton<AuthRepositoryInterface>(
() => AuthRepositoryImpl(apiService: i.get<BaseApiService>()),
() => AuthRepositoryImpl(
apiService: i.get<BaseApiService>(),
firebaseAuthService: i.get<FirebaseAuthService>(),
),
);
// UseCases

View File

@@ -1,7 +1,6 @@
import 'dart:developer' as developer;
import 'package:client_authentication/src/domain/repositories/auth_repository_interface.dart';
import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart'
show
@@ -10,7 +9,6 @@ import 'package:krow_domain/krow_domain.dart'
AppException,
BaseApiService,
ClientSession,
InvalidCredentialsException,
NetworkException,
PasswordMismatchException,
SignInFailedException,
@@ -21,20 +19,23 @@ import 'package:krow_domain/krow_domain.dart'
/// Production implementation of the [AuthRepositoryInterface] for the client app.
///
/// Uses Firebase Auth client-side for sign-in (to maintain local auth state for
/// the [AuthInterceptor]), then calls V2 `GET /auth/session` to retrieve
/// business context. Sign-up provisioning (tenant, business, memberships) is
/// handled entirely server-side by the V2 API.
/// Uses [FirebaseAuthService] from core for local Firebase sign-in (to maintain
/// local auth state for the [AuthInterceptor]), then calls V2 `GET /auth/session`
/// to retrieve business context. Sign-up provisioning (tenant, business,
/// memberships) is handled entirely server-side by the V2 API.
class AuthRepositoryImpl implements AuthRepositoryInterface {
/// Creates an [AuthRepositoryImpl] with the given [BaseApiService].
AuthRepositoryImpl({required BaseApiService apiService})
: _apiService = apiService;
/// Creates an [AuthRepositoryImpl] with the given dependencies.
AuthRepositoryImpl({
required BaseApiService apiService,
required FirebaseAuthService firebaseAuthService,
}) : _apiService = apiService,
_firebaseAuthService = firebaseAuthService;
/// The V2 API service for backend calls.
final BaseApiService _apiService;
/// Firebase Auth instance for client-side sign-in/sign-up.
firebase.FirebaseAuth get _auth => firebase.FirebaseAuth.instance;
/// Core Firebase Auth service abstraction.
final FirebaseAuthService _firebaseAuthService;
@override
Future<User> signInWithEmail({
@@ -42,38 +43,26 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
required String password,
}) async {
try {
// Step 1: Call V2 sign-in endpoint server handles Firebase Auth
// Step 1: Call V2 sign-in endpoint -- server handles Firebase Auth
// via Identity Toolkit and returns a full auth envelope.
final ApiResponse response = await _apiService.post(
AuthEndpoints.clientSignIn,
data: <String, dynamic>{
'email': email,
'password': password,
},
data: <String, dynamic>{'email': email, 'password': password},
);
final Map<String, dynamic> body =
response.data as Map<String, dynamic>;
final Map<String, dynamic> body = response.data as Map<String, dynamic>;
// Step 2: Sign in locally so AuthInterceptor can attach Bearer tokens
// to subsequent requests. The V2 API already validated credentials, so
// email/password sign-in establishes the local Firebase Auth state.
final firebase.UserCredential credential =
await _auth.signInWithEmailAndPassword(
await _firebaseAuthService.signInWithEmailAndPassword(
email: email,
password: password,
);
final firebase.User? firebaseUser = credential.user;
if (firebaseUser == null) {
throw const SignInFailedException(
technicalMessage: 'Local Firebase sign-in failed after V2 sign-in',
);
}
// Step 3: Populate session store from the V2 auth envelope directly
// (no need for a separate GET /auth/session call).
return _populateStoreFromAuthEnvelope(body, firebaseUser, email);
return _populateStoreFromAuthEnvelope(body, email);
} on AppException {
rethrow;
} catch (e) {
@@ -106,38 +95,34 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
// Step 2: Sign in locally to Firebase Auth so AuthInterceptor works
// for subsequent requests. The V2 API already created the Firebase
// account, so this should succeed.
final firebase.UserCredential credential =
await _auth.signInWithEmailAndPassword(
email: email,
password: password,
);
final firebase.User? firebaseUser = credential.user;
if (firebaseUser == null) {
try {
await _firebaseAuthService.signInWithEmailAndPassword(
email: email,
password: password,
);
} on SignInFailedException {
throw const SignUpFailedException(
technicalMessage: 'Local Firebase sign-in failed after V2 sign-up',
);
}
// Step 3: Populate store from the sign-up response envelope.
return _populateStoreFromAuthEnvelope(body, firebaseUser, email);
} on firebase.FirebaseAuthException catch (e) {
if (e.code == 'email-already-in-use') {
throw AccountExistsException(
technicalMessage: 'Firebase: ${e.message}',
);
} else if (e.code == 'weak-password') {
throw WeakPasswordException(technicalMessage: 'Firebase: ${e.message}');
} else if (e.code == 'network-request-failed') {
throw NetworkException(technicalMessage: 'Firebase: ${e.message}');
} else {
throw SignUpFailedException(
technicalMessage: 'Firebase auth error: ${e.message}',
);
}
return _populateStoreFromAuthEnvelope(body, email);
} on AppException {
rethrow;
} catch (e) {
// Map common Firebase-originated errors from the V2 API response
// to domain exceptions.
final String errorMessage = e.toString();
if (errorMessage.contains('EMAIL_EXISTS') ||
errorMessage.contains('email-already-in-use')) {
throw AccountExistsException(technicalMessage: errorMessage);
} else if (errorMessage.contains('WEAK_PASSWORD') ||
errorMessage.contains('weak-password')) {
throw WeakPasswordException(technicalMessage: errorMessage);
} else if (errorMessage.contains('network-request-failed')) {
throw NetworkException(technicalMessage: errorMessage);
}
throw SignUpFailedException(technicalMessage: 'Unexpected error: $e');
}
}
@@ -155,16 +140,13 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
// Step 1: Call V2 sign-out endpoint for server-side token revocation.
await _apiService.post(AuthEndpoints.clientSignOut);
} catch (e) {
developer.log(
'V2 sign-out request failed: $e',
name: 'AuthRepository',
);
developer.log('V2 sign-out request failed: $e', name: 'AuthRepository');
// Continue with local sign-out even if server-side fails.
}
try {
// Step 2: Sign out from local Firebase Auth.
await _auth.signOut();
// Step 2: Sign out from local Firebase Auth via core service.
await _firebaseAuthService.signOut();
} catch (e) {
throw Exception('Error signing out locally: $e');
}
@@ -181,7 +163,6 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
/// returns a domain [User].
User _populateStoreFromAuthEnvelope(
Map<String, dynamic> envelope,
firebase.User firebaseUser,
String fallbackEmail,
) {
final Map<String, dynamic>? userJson =
@@ -202,14 +183,15 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
'userId': userJson['id'] ?? userJson['userId'],
},
};
final ClientSession clientSession =
ClientSession.fromJson(normalisedEnvelope);
final ClientSession clientSession = ClientSession.fromJson(
normalisedEnvelope,
);
ClientSessionStore.instance.setSession(clientSession);
}
final String userId =
userJson?['id'] as String? ?? firebaseUser.uid;
final String? email = userJson?['email'] as String? ?? fallbackEmail;
final String userId = userJson?['id'] as String? ??
(_firebaseAuthService.currentUserUid ?? '');
final String email = userJson?['email'] as String? ?? fallbackEmail;
return User(
id: userId,