|
|
|
|
@@ -1,6 +1,5 @@
|
|
|
|
|
import 'dart:async';
|
|
|
|
|
|
|
|
|
|
import 'package:firebase_auth/firebase_auth.dart';
|
|
|
|
|
import 'package:krow_core/core.dart';
|
|
|
|
|
import 'package:krow_domain/krow_domain.dart' as domain;
|
|
|
|
|
import 'package:staff_authentication/src/domain/repositories/auth_repository_interface.dart';
|
|
|
|
|
@@ -9,55 +8,42 @@ import 'package:staff_authentication/src/utils/test_phone_numbers.dart';
|
|
|
|
|
|
|
|
|
|
/// V2 API implementation of [AuthRepositoryInterface].
|
|
|
|
|
///
|
|
|
|
|
/// Uses the Firebase Auth SDK for client-side phone verification,
|
|
|
|
|
/// Uses [FirebaseAuthService] from core for client-side phone verification,
|
|
|
|
|
/// then calls the V2 unified API to hydrate the session context.
|
|
|
|
|
/// All Data Connect dependencies have been removed.
|
|
|
|
|
/// All direct `firebase_auth` imports have been removed in favour of the
|
|
|
|
|
/// core abstraction.
|
|
|
|
|
class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|
|
|
|
/// Creates an [AuthRepositoryImpl].
|
|
|
|
|
///
|
|
|
|
|
/// Requires a [domain.BaseApiService] for V2 API calls.
|
|
|
|
|
AuthRepositoryImpl({required domain.BaseApiService apiService})
|
|
|
|
|
: _apiService = apiService;
|
|
|
|
|
/// Requires a [domain.BaseApiService] for V2 API calls and a
|
|
|
|
|
/// [FirebaseAuthService] for client-side Firebase Auth operations.
|
|
|
|
|
AuthRepositoryImpl({
|
|
|
|
|
required domain.BaseApiService apiService,
|
|
|
|
|
required FirebaseAuthService firebaseAuthService,
|
|
|
|
|
}) : _apiService = apiService,
|
|
|
|
|
_firebaseAuthService = firebaseAuthService;
|
|
|
|
|
|
|
|
|
|
/// The V2 API service for backend calls.
|
|
|
|
|
final domain.BaseApiService _apiService;
|
|
|
|
|
|
|
|
|
|
/// Firebase Auth instance for client-side phone verification.
|
|
|
|
|
final FirebaseAuth _auth = FirebaseAuth.instance;
|
|
|
|
|
|
|
|
|
|
/// Completer for the pending phone verification request.
|
|
|
|
|
Completer<String?>? _pendingVerification;
|
|
|
|
|
/// Core Firebase Auth service abstraction.
|
|
|
|
|
final FirebaseAuthService _firebaseAuthService;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Stream<domain.User?> get currentUser =>
|
|
|
|
|
_auth.authStateChanges().map((User? firebaseUser) {
|
|
|
|
|
if (firebaseUser == null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return domain.User(
|
|
|
|
|
id: firebaseUser.uid,
|
|
|
|
|
email: firebaseUser.email,
|
|
|
|
|
displayName: firebaseUser.displayName,
|
|
|
|
|
phone: firebaseUser.phoneNumber,
|
|
|
|
|
status: domain.UserStatus.active,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
Stream<domain.User?> get currentUser => _firebaseAuthService.authStateChanges;
|
|
|
|
|
|
|
|
|
|
/// Initiates phone verification via the V2 API.
|
|
|
|
|
///
|
|
|
|
|
/// Calls `POST /auth/staff/phone/start` first. The server decides the
|
|
|
|
|
/// verification mode:
|
|
|
|
|
/// - `CLIENT_FIREBASE_SDK` — mobile must do Firebase phone auth client-side
|
|
|
|
|
/// - `IDENTITY_TOOLKIT_SMS` — server sent the SMS, returns `sessionInfo`
|
|
|
|
|
/// - `CLIENT_FIREBASE_SDK` -- mobile must do Firebase phone auth client-side
|
|
|
|
|
/// - `IDENTITY_TOOLKIT_SMS` -- server sent the SMS, returns `sessionInfo`
|
|
|
|
|
///
|
|
|
|
|
/// For mobile without recaptcha tokens, the server returns
|
|
|
|
|
/// `CLIENT_FIREBASE_SDK` and we fall back to the Firebase Auth SDK.
|
|
|
|
|
@override
|
|
|
|
|
Future<String?> signInWithPhone({required String phoneNumber}) async {
|
|
|
|
|
// Step 1: Try V2 to let the server decide the auth mode.
|
|
|
|
|
// Falls back to CLIENT_FIREBASE_SDK if the API call fails (e.g. server
|
|
|
|
|
// down, 500, or non-JSON response).
|
|
|
|
|
String mode = 'CLIENT_FIREBASE_SDK';
|
|
|
|
|
String? sessionInfo;
|
|
|
|
|
|
|
|
|
|
@@ -74,7 +60,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|
|
|
|
mode = startData['mode'] as String? ?? 'CLIENT_FIREBASE_SDK';
|
|
|
|
|
sessionInfo = startData['sessionInfo'] as String?;
|
|
|
|
|
} catch (_) {
|
|
|
|
|
// V2 start call failed — fall back to client-side Firebase SDK.
|
|
|
|
|
// V2 start call failed -- fall back to client-side Firebase SDK.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 2: If server sent the SMS, return the sessionInfo for verify step.
|
|
|
|
|
@@ -82,55 +68,16 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|
|
|
|
return sessionInfo;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 3: CLIENT_FIREBASE_SDK mode — do Firebase phone auth client-side.
|
|
|
|
|
final Completer<String?> completer = Completer<String?>();
|
|
|
|
|
_pendingVerification = completer;
|
|
|
|
|
|
|
|
|
|
await _auth.verifyPhoneNumber(
|
|
|
|
|
// Step 3: CLIENT_FIREBASE_SDK mode -- do Firebase phone auth client-side.
|
|
|
|
|
return _firebaseAuthService.verifyPhoneNumber(
|
|
|
|
|
phoneNumber: phoneNumber,
|
|
|
|
|
verificationCompleted: (PhoneAuthCredential credential) {
|
|
|
|
|
if (TestPhoneNumbers.isTestNumber(phoneNumber)) return;
|
|
|
|
|
},
|
|
|
|
|
verificationFailed: (FirebaseAuthException e) {
|
|
|
|
|
if (!completer.isCompleted) {
|
|
|
|
|
if (e.code == 'network-request-failed' ||
|
|
|
|
|
e.message?.contains('Unable to resolve host') == true) {
|
|
|
|
|
completer.completeError(
|
|
|
|
|
const domain.NetworkException(
|
|
|
|
|
technicalMessage: 'Auth network failure',
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
completer.completeError(
|
|
|
|
|
domain.SignInFailedException(
|
|
|
|
|
technicalMessage: 'Firebase ${e.code}: ${e.message}',
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
codeSent: (String verificationId, _) {
|
|
|
|
|
if (!completer.isCompleted) {
|
|
|
|
|
completer.complete(verificationId);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
codeAutoRetrievalTimeout: (String verificationId) {
|
|
|
|
|
if (!completer.isCompleted) {
|
|
|
|
|
completer.complete(verificationId);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onAutoVerified: TestPhoneNumbers.isTestNumber(phoneNumber) ? null : null,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return completer.future;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void cancelPendingPhoneVerification() {
|
|
|
|
|
final Completer<String?>? completer = _pendingVerification;
|
|
|
|
|
if (completer != null && !completer.isCompleted) {
|
|
|
|
|
completer.completeError(Exception('Phone verification cancelled.'));
|
|
|
|
|
}
|
|
|
|
|
_pendingVerification = null;
|
|
|
|
|
_firebaseAuthService.cancelPendingPhoneVerification();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Verifies the OTP and completes authentication via the V2 API.
|
|
|
|
|
@@ -145,53 +92,26 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|
|
|
|
required String smsCode,
|
|
|
|
|
required AuthMode mode,
|
|
|
|
|
}) async {
|
|
|
|
|
// Step 1: Sign in with Firebase credential (client-side).
|
|
|
|
|
final PhoneAuthCredential credential = PhoneAuthProvider.credential(
|
|
|
|
|
// Step 1: Sign in with Firebase credential via core service.
|
|
|
|
|
final PhoneSignInResult signInResult =
|
|
|
|
|
await _firebaseAuthService.signInWithPhoneCredential(
|
|
|
|
|
verificationId: verificationId,
|
|
|
|
|
smsCode: smsCode,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
final UserCredential userCredential;
|
|
|
|
|
try {
|
|
|
|
|
userCredential = await _auth.signInWithCredential(credential);
|
|
|
|
|
} on FirebaseAuthException catch (e) {
|
|
|
|
|
if (e.code == 'invalid-verification-code') {
|
|
|
|
|
throw const domain.InvalidCredentialsException(
|
|
|
|
|
technicalMessage: 'Invalid OTP code entered.',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
rethrow;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final User? firebaseUser = userCredential.user;
|
|
|
|
|
if (firebaseUser == null) {
|
|
|
|
|
throw const domain.SignInFailedException(
|
|
|
|
|
technicalMessage:
|
|
|
|
|
'Phone verification failed, no Firebase user received.',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 2: Get the Firebase ID token.
|
|
|
|
|
final String? idToken = await firebaseUser.getIdToken();
|
|
|
|
|
if (idToken == null) {
|
|
|
|
|
throw const domain.SignInFailedException(
|
|
|
|
|
technicalMessage: 'Failed to obtain Firebase ID token.',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 3: Call V2 verify endpoint with the Firebase ID token.
|
|
|
|
|
// Step 2: Call V2 verify endpoint with the Firebase ID token.
|
|
|
|
|
final String v2Mode = mode == AuthMode.signup ? 'sign-up' : 'sign-in';
|
|
|
|
|
final domain.ApiResponse response = await _apiService.post(
|
|
|
|
|
AuthEndpoints.staffPhoneVerify,
|
|
|
|
|
data: <String, dynamic>{
|
|
|
|
|
'idToken': idToken,
|
|
|
|
|
'idToken': signInResult.idToken,
|
|
|
|
|
'mode': v2Mode,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
final Map<String, dynamic> data = response.data as Map<String, dynamic>;
|
|
|
|
|
|
|
|
|
|
// Step 4: Check for business logic errors from the V2 API.
|
|
|
|
|
// Step 3: Check for business logic errors from the V2 API.
|
|
|
|
|
final Map<String, dynamic>? staffData =
|
|
|
|
|
data['staff'] as Map<String, dynamic>?;
|
|
|
|
|
final Map<String, dynamic>? userData =
|
|
|
|
|
@@ -202,7 +122,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|
|
|
|
// - Sign-in: staff must exist
|
|
|
|
|
if (mode == AuthMode.login) {
|
|
|
|
|
if (staffData == null) {
|
|
|
|
|
await _auth.signOut();
|
|
|
|
|
await _firebaseAuthService.signOut();
|
|
|
|
|
throw const domain.UserNotFoundException(
|
|
|
|
|
technicalMessage:
|
|
|
|
|
'Your account is not registered yet. Please register first.',
|
|
|
|
|
@@ -210,7 +130,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 5: Populate StaffSessionStore from the V2 auth envelope.
|
|
|
|
|
// Step 4: Populate StaffSessionStore from the V2 auth envelope.
|
|
|
|
|
if (staffData != null) {
|
|
|
|
|
final domain.StaffSession staffSession =
|
|
|
|
|
domain.StaffSession.fromJson(data);
|
|
|
|
|
@@ -219,10 +139,10 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|
|
|
|
|
|
|
|
|
// Build the domain user from the V2 response.
|
|
|
|
|
final domain.User domainUser = domain.User(
|
|
|
|
|
id: userData?['id'] as String? ?? firebaseUser.uid,
|
|
|
|
|
id: userData?['id'] as String? ?? signInResult.uid,
|
|
|
|
|
email: userData?['email'] as String?,
|
|
|
|
|
displayName: userData?['displayName'] as String?,
|
|
|
|
|
phone: userData?['phone'] as String? ?? firebaseUser.phoneNumber,
|
|
|
|
|
phone: userData?['phone'] as String? ?? signInResult.phoneNumber,
|
|
|
|
|
status: domain.UserStatus.active,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
@@ -238,7 +158,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
|
|
|
|
// Sign-out should not fail even if the API call fails.
|
|
|
|
|
// The local sign-out below will clear the session regardless.
|
|
|
|
|
}
|
|
|
|
|
await _auth.signOut();
|
|
|
|
|
await _firebaseAuthService.signOut();
|
|
|
|
|
StaffSessionStore.instance.clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|