diff --git a/apps/mobile/apps/staff/android/app/build.gradle.kts b/apps/mobile/apps/staff/android/app/build.gradle.kts index 0f75ed01..80f2b222 100644 --- a/apps/mobile/apps/staff/android/app/build.gradle.kts +++ b/apps/mobile/apps/staff/android/app/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("kotlin-android") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") + id("com.google.gms.google-services") } android { diff --git a/apps/mobile/apps/staff/android/app/google-services.json b/apps/mobile/apps/staff/android/app/google-services.json new file mode 100644 index 00000000..13b4592b --- /dev/null +++ b/apps/mobile/apps/staff/android/app/google-services.json @@ -0,0 +1,226 @@ +{ + "project_info": { + "project_number": "933560802882", + "project_id": "krow-workforce-dev", + "storage_bucket": "krow-workforce-dev.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:933560802882:android:87d41566f8dda41d7757db", + "android_client_info": { + "package_name": "com.example.krow_workforce" + } + }, + "oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.krow.app.staff.dev" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:933560802882:android:edcddb83ea4bbb517757db", + "android_client_info": { + "package_name": "com.krow.app.business.dev" + } + }, + "oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.krow.app.staff.dev" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:933560802882:android:d49b8c0f4d19e95e7757db", + "android_client_info": { + "package_name": "com.krow.app.staff.dev" + } + }, + "oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.krow.app.staff.dev" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:933560802882:android:da13569105659ead7757db", + "android_client_info": { + "package_name": "com.krowwithus.client" + } + }, + "oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.krow.app.staff.dev" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:933560802882:android:d26bde4ee337b0b17757db", + "android_client_info": { + "package_name": "com.krowwithus.krow_workforce.dev" + } + }, + "oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.krow.app.staff.dev" + } + } + ] + } + } + }, + { + "client_info": { + "mobilesdk_app_id": "1:933560802882:android:1ae05d85c865f77c7757db", + "android_client_info": { + "package_name": "com.krowwithus.staff" + } + }, + "oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDBYhflhK6DThKnS7RM-9raKdvyKzLUjY4" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com", + "client_type": 3 + }, + { + "client_id": "933560802882-dppsapp5i3lsfrlm1mhob2s21peofg1t.apps.googleusercontent.com", + "client_type": 2, + "ios_info": { + "bundle_id": "com.krow.app.staff.dev" + } + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/apps/mobile/apps/staff/android/settings.gradle.kts b/apps/mobile/apps/staff/android/settings.gradle.kts index ca7fe065..e4e86fb6 100644 --- a/apps/mobile/apps/staff/android/settings.gradle.kts +++ b/apps/mobile/apps/staff/android/settings.gradle.kts @@ -21,6 +21,7 @@ plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "8.11.1" apply false id("org.jetbrains.kotlin.android") version "2.2.20" apply false + id("com.google.gms.google-services") version "4.4.2" apply false } include(":app") diff --git a/apps/mobile/apps/staff/lib/main.dart b/apps/mobile/apps/staff/lib/main.dart index cbfcaf74..63d82ff0 100644 --- a/apps/mobile/apps/staff/lib/main.dart +++ b/apps/mobile/apps/staff/lib/main.dart @@ -6,10 +6,11 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:staff_authentication/staff_authentication.dart' as staff_authentication; +import 'package:firebase_core/firebase_core.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - + await Firebase.initializeApp(); runApp(ModularApp(module: AppModule(), child: const AppWidget())); } diff --git a/apps/mobile/packages/data_connect/lib/krow_data_connect.dart b/apps/mobile/packages/data_connect/lib/krow_data_connect.dart index 445db229..a67d149b 100644 --- a/apps/mobile/packages/data_connect/lib/krow_data_connect.dart +++ b/apps/mobile/packages/data_connect/lib/krow_data_connect.dart @@ -18,6 +18,7 @@ export 'src/mocks/home_repository_mock.dart'; export 'src/mocks/business_repository_mock.dart'; export 'src/mocks/order_repository_mock.dart'; export 'src/data_connect_module.dart'; +export 'src/session/client_session_store.dart'; // Export the generated Data Connect SDK export 'src/dataconnect_generated/generated.dart'; diff --git a/apps/mobile/packages/data_connect/lib/src/session/client_session_store.dart b/apps/mobile/packages/data_connect/lib/src/session/client_session_store.dart new file mode 100644 index 00000000..e17f22a4 --- /dev/null +++ b/apps/mobile/packages/data_connect/lib/src/session/client_session_store.dart @@ -0,0 +1,49 @@ +import 'package:krow_domain/krow_domain.dart' as domain; + +class ClientBusinessSession { + final String id; + final String businessName; + final String? email; + final String? city; + final String? contactName; + final String? companyLogoUrl; + + const ClientBusinessSession({ + required this.id, + required this.businessName, + this.email, + this.city, + this.contactName, + this.companyLogoUrl, + }); +} + +class ClientSession { + final domain.User user; + final String? userPhotoUrl; + final ClientBusinessSession? business; + + const ClientSession({ + required this.user, + required this.userPhotoUrl, + required this.business, + }); +} + +class ClientSessionStore { + ClientSession? _session; + + ClientSession? get session => _session; + + void setSession(ClientSession session) { + _session = session; + } + + void clear() { + _session = null; + } + + static final ClientSessionStore instance = ClientSessionStore._(); + + ClientSessionStore._(); +} diff --git a/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart b/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart index ef697865..0756527f 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart @@ -1,5 +1,5 @@ -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:krow_data_connect/krow_data_connect.dart'; +import 'package:firebase_auth/firebase_auth.dart' as firebase; +import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_domain/krow_domain.dart' as domain; import '../../domain/repositories/auth_repository_interface.dart'; @@ -8,15 +8,15 @@ import '../../domain/repositories/auth_repository_interface.dart'; /// This implementation integrates with Firebase Authentication for user /// identity management and Krow's Data Connect SDK for storing user profile data. class AuthRepositoryImpl implements AuthRepositoryInterface { - final FirebaseAuth _firebaseAuth; - final ExampleConnector _dataConnect; + final firebase.FirebaseAuth _firebaseAuth; + final dc.ExampleConnector _dataConnect; /// Creates an [AuthRepositoryImpl] with the real dependencies. AuthRepositoryImpl({ - required FirebaseAuth firebaseAuth, - required ExampleConnector dataConnect, - }) : _firebaseAuth = firebaseAuth, - _dataConnect = dataConnect; + required firebase.FirebaseAuth firebaseAuth, + required dc.ExampleConnector dataConnect, + }) : _firebaseAuth = firebaseAuth, + _dataConnect = dataConnect; @override Future signInWithEmail({ @@ -40,7 +40,8 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { ); //TO-DO: validate that user is business role and has business account - } on FirebaseAuthException catch (e) { + + } on firebase.FirebaseAuthException catch (e) { if (e.code == 'invalid-credential' || e.code == 'wrong-password') { throw Exception('Incorrect email or password.'); } else { @@ -71,25 +72,23 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { // Client-specific business logic: // 1. Create a `Business` entity. // 2. Create a `User` entity associated with the business. - final createBusinessResponse = await _dataConnect - .createBusiness( - businessName: companyName, - userId: firebaseUser.uid, - rateGroup: BusinessRateGroup.STANDARD, - status: BusinessStatus.PENDING, - ) - .execute(); + final createBusinessResponse = await _dataConnect.createBusiness( + businessName: companyName, + userId: firebaseUser.uid, + rateGroup: dc.BusinessRateGroup.STANDARD, + status: dc.BusinessStatus.PENDING, + ).execute(); final businessData = createBusinessResponse.data?.business_insert; if (businessData == null) { await firebaseUser.delete(); // Rollback if business creation fails - throw Exception( - 'Business creation failed after Firebase user registration.', - ); + throw Exception('Business creation failed after Firebase user registration.'); } - final createUserResponse = await _dataConnect - .createUser(id: firebaseUser.uid, role: UserBaseRole.USER) + final createUserResponse = await _dataConnect.createUser( + id: firebaseUser.uid, + role: dc.UserBaseRole.USER, + ) .email(email) .userRole('BUSINESS') .execute(); @@ -98,16 +97,15 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { if (newUserData == null) { await firebaseUser.delete(); // Rollback if user profile creation fails // TO-DO: Also delete the created Business if this fails - throw Exception( - 'User profile creation failed after Firebase user registration.', - ); + throw Exception('User profile creation failed after Firebase user registration.'); } return _getUserProfile( firebaseUserId: firebaseUser.uid, fallbackEmail: firebaseUser.email ?? email, ); - } on FirebaseAuthException catch (e) { + + } on firebase.FirebaseAuthException catch (e) { if (e.code == 'weak-password') { throw Exception('The password provided is too weak.'); } else if (e.code == 'email-already-in-use') { @@ -116,9 +114,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { throw Exception('Sign-up error: ${e.message}'); } } catch (e) { - throw Exception( - 'Failed to sign up and create user data: ${e.toString()}', - ); + throw Exception('Failed to sign up and create user data: ${e.toString()}'); } } @@ -126,6 +122,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { Future signOut() async { try { await _firebaseAuth.signOut(); + dc.ClientSessionStore.instance.clear(); } catch (e) { throw Exception('Error signing out: ${e.toString()}'); } @@ -133,18 +130,14 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { @override Future signInWithSocial({required String provider}) { - throw UnimplementedError( - 'Social authentication with $provider is not yet implemented.', - ); + throw UnimplementedError('Social authentication with $provider is not yet implemented.'); } Future _getUserProfile({ required String firebaseUserId, required String? fallbackEmail, }) async { - final response = await _dataConnect - .getUserById(id: firebaseUserId) - .execute(); + final response = await _dataConnect.getUserById(id: firebaseUserId).execute(); final user = response.data?.user; if (user == null) { throw Exception('Authenticated user profile not found in database.'); @@ -155,6 +148,36 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { throw Exception('User email is missing in profile data.'); } - return domain.User(id: user.id, email: email, role: user.role.stringValue); + final domainUser = domain.User( + id: user.id, + email: email, + role: user.role.stringValue, + ); + + final businessResponse = await _dataConnect.getBusinessesByUserId( + userId: firebaseUserId, + ).execute(); + final business = businessResponse.data.businesses.isNotEmpty + ? businessResponse.data.businesses.first + : null; + + dc.ClientSessionStore.instance.setSession( + dc.ClientSession( + user: domainUser, + userPhotoUrl: user.photoUrl, + business: business == null + ? null + : dc.ClientBusinessSession( + id: business.id, + businessName: business.businessName, + email: business.email, + city: business.city, + contactName: business.contactName, + companyLogoUrl: business.companyLogoUrl, + ), + ), + ); + + return domainUser; } } diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/pages/client_home_page.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/pages/client_home_page.dart index 6f3840df..32c698d8 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/pages/client_home_page.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/pages/client_home_page.dart @@ -3,6 +3,7 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_data_connect/krow_data_connect.dart' as dc; import '../blocs/client_home_bloc.dart'; import '../blocs/client_home_event.dart'; @@ -116,6 +117,14 @@ class ClientHomePage extends StatelessWidget { Widget _buildHeader(BuildContext context, dynamic i18n) { return BlocBuilder( builder: (context, state) { + final session = dc.ClientSessionStore.instance.session; + final businessName = + session?.business?.businessName ?? 'Your Company'; + final photoUrl = session?.userPhotoUrl; + final avatarLetter = businessName.trim().isNotEmpty + ? businessName.trim()[0].toUpperCase() + : 'C'; + return Padding( padding: const EdgeInsets.fromLTRB( UiConstants.space4, @@ -140,12 +149,19 @@ class ClientHomePage extends StatelessWidget { ), child: CircleAvatar( backgroundColor: UiColors.primary.withValues(alpha: 0.1), - child: Text( - 'C', - style: UiTypography.body2b.copyWith( - color: UiColors.primary, - ), - ), + backgroundImage: + photoUrl != null && photoUrl.isNotEmpty + ? NetworkImage(photoUrl) + : null, + child: + photoUrl != null && photoUrl.isNotEmpty + ? null + : Text( + avatarLetter, + style: UiTypography.body2b.copyWith( + color: UiColors.primary, + ), + ), ), ), const SizedBox(width: UiConstants.space3), @@ -156,7 +172,7 @@ class ClientHomePage extends StatelessWidget { i18n.dashboard.welcome_back, style: UiTypography.footnote2r.textSecondary, ), - Text('Your Company', style: UiTypography.body1b), + Text(businessName, style: UiTypography.body1b), ], ), ], diff --git a/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart b/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart index 1fee40f5..3de1e29a 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart @@ -79,6 +79,30 @@ class HubRepositoryImpl implements HubRepositoryInterface { } Future _getBusinessForCurrentUser() async { + final session = dc.ClientSessionStore.instance.session; + final cachedBusiness = session?.business; + if (cachedBusiness != null) { + return dc.GetBusinessesByUserIdBusinesses( + id: cachedBusiness.id, + businessName: cachedBusiness.businessName, + userId: _firebaseAuth.currentUser?.uid ?? '', + rateGroup: dc.Known(dc.BusinessRateGroup.STANDARD), + status: dc.Known(dc.BusinessStatus.ACTIVE), + contactName: cachedBusiness.contactName, + companyLogoUrl: cachedBusiness.companyLogoUrl, + phone: null, + email: cachedBusiness.email, + hubBuilding: null, + address: null, + city: cachedBusiness.city, + area: null, + sector: null, + notes: null, + createdAt: null, + updatedAt: null, + ); + } + final user = _firebaseAuth.currentUser; if (user == null) { throw Exception('User is not authenticated.'); @@ -92,7 +116,25 @@ class HubRepositoryImpl implements HubRepositoryInterface { throw Exception('No business found for this user. Please sign in again.'); } - return result.data.businesses.first; + final business = result.data.businesses.first; + if (session != null) { + dc.ClientSessionStore.instance.setSession( + dc.ClientSession( + user: session.user, + userPhotoUrl: session.userPhotoUrl, + business: dc.ClientBusinessSession( + id: business.id, + businessName: business.businessName, + email: business.email, + city: business.city, + contactName: business.contactName, + companyLogoUrl: business.companyLogoUrl, + ), + ), + ); + } + + return business; } Future _getOrCreateTeamId( diff --git a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart index 48f6a57f..9f795f35 100644 --- a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart +++ b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart @@ -2,6 +2,7 @@ import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_data_connect/krow_data_connect.dart' as dc; /// A widget that displays the profile header with avatar and company info. class SettingsProfileHeader extends StatelessWidget { @@ -12,6 +13,14 @@ class SettingsProfileHeader extends StatelessWidget { /// Builds the profile header UI. Widget build(BuildContext context) { final labels = t.client_settings.profile; + final session = dc.ClientSessionStore.instance.session; + final businessName = + session?.business?.businessName ?? 'Your Company'; + final email = session?.user.email ?? 'client@example.com'; + final photoUrl = session?.userPhotoUrl; + final avatarLetter = businessName.trim().isNotEmpty + ? businessName.trim()[0].toUpperCase() + : 'C'; return SliverAppBar( backgroundColor: UiColors.bgSecondary, @@ -40,20 +49,28 @@ class SettingsProfileHeader extends StatelessWidget { border: Border.all(color: UiColors.border, width: 2), color: UiColors.white, ), - child: Center( - child: Text( - 'C', - style: UiTypography.headline1m.copyWith( - color: UiColors.primary, - ), - ), + child: CircleAvatar( + backgroundColor: UiColors.primary.withValues(alpha: 0.1), + backgroundImage: + photoUrl != null && photoUrl.isNotEmpty + ? NetworkImage(photoUrl) + : null, + child: + photoUrl != null && photoUrl.isNotEmpty + ? null + : Text( + avatarLetter, + style: UiTypography.headline1m.copyWith( + color: UiColors.primary, + ), + ), ), ), Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('Your Company', style: UiTypography.body1b.textPrimary), + Text(businessName, style: UiTypography.body1b.textPrimary), const SizedBox(height: UiConstants.space1), Row( mainAxisAlignment: MainAxisAlignment.start, @@ -65,7 +82,7 @@ class SettingsProfileHeader extends StatelessWidget { color: UiColors.textSecondary, ), Text( - 'client@example.com', + email, style: UiTypography.footnote1r.textSecondary, ), ], diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart b/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart index d8935c46..a399956d 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart @@ -1,34 +1,116 @@ -import 'package:krow_data_connect/krow_data_connect.dart'; -import 'package:krow_domain/krow_domain.dart'; +import 'dart:async'; + +import 'package:firebase_auth/firebase_auth.dart' as firebase; +import 'package:krow_data_connect/krow_data_connect.dart' as dc; +import 'package:krow_domain/krow_domain.dart' as domain; import '../../domain/repositories/auth_repository_interface.dart'; /// Implementation of [AuthRepositoryInterface]. class AuthRepositoryImpl implements AuthRepositoryInterface { - final AuthRepositoryMock mock; + final firebase.FirebaseAuth _firebaseAuth; + final dc.ExampleConnector _dataConnect; - AuthRepositoryImpl({required this.mock}); + AuthRepositoryImpl({ + required firebase.FirebaseAuth firebaseAuth, + required dc.ExampleConnector dataConnect, + }) : _firebaseAuth = firebaseAuth, + _dataConnect = dataConnect; @override - Stream get currentUser => mock.currentUser; + Stream get currentUser => _firebaseAuth + .authStateChanges() + .map((firebaseUser) { + if (firebaseUser == null) { + return null; + } + + return domain.User( + id: firebaseUser.uid, + email: firebaseUser.email ?? '', + phone: firebaseUser.phoneNumber, + role: 'staff', + ); + }); /// Signs in with a phone number and returns a verification ID. @override - Future signInWithPhone({required String phoneNumber}) { - return mock.signInWithPhone(phoneNumber); + Future signInWithPhone({required String phoneNumber}) async { + final completer = Completer(); + + await _firebaseAuth.verifyPhoneNumber( + phoneNumber: phoneNumber, + verificationCompleted: (_) {}, + verificationFailed: (e) { + if (!completer.isCompleted) { + completer.completeError( + Exception(e.message ?? 'Phone verification failed.'), + ); + } + }, + codeSent: (verificationId, _) { + if (!completer.isCompleted) { + completer.complete(verificationId); + } + }, + codeAutoRetrievalTimeout: (verificationId) { + if (!completer.isCompleted) { + completer.complete(verificationId); + } + }, + ); + + return completer.future; } /// Signs out the current user. @override Future signOut() { - return mock.signOut(); + return _firebaseAuth.signOut(); } /// Verifies an OTP code and returns the authenticated user. @override - Future verifyOtp({ + Future verifyOtp({ required String verificationId, required String smsCode, - }) { - return mock.verifyOtp(verificationId, smsCode); + }) async { + final credential = firebase.PhoneAuthProvider.credential( + verificationId: verificationId, + smsCode: smsCode, + ); + final userCredential = await _firebaseAuth.signInWithCredential(credential); + final firebaseUser = userCredential.user; + if (firebaseUser == null) { + throw Exception('Phone verification failed, no Firebase user received.'); + } + + final response = await _dataConnect.getUserById( + id: firebaseUser.uid, + ).execute(); + final user = response.data?.user; + if (user == null) { + await _firebaseAuth.signOut(); + throw Exception('Authenticated user profile not found in database.'); + } + if (user.userRole != 'STAFF') { + await _firebaseAuth.signOut(); + throw Exception('User is not authorized for this app.'); + } + + final email = user.email ?? ''; + if (email.isEmpty) { + await _firebaseAuth.signOut(); + throw Exception('User email is missing in profile data.'); + } + + //TO-DO: validate if user has staff account, else logout, throw message and login + //TO-DO: create(registration) user and staff account + //TO-DO: save user data locally + return domain.User( + id: user.id, + email: email, + phone: firebaseUser.phoneNumber, + role: user.role.stringValue, + ); } } diff --git a/apps/mobile/packages/features/staff/authentication/lib/staff_authentication.dart b/apps/mobile/packages/features/staff/authentication/lib/staff_authentication.dart index 2c272187..ab9bc99a 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/staff_authentication.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/staff_authentication.dart @@ -2,6 +2,7 @@ library staff_authentication; import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_data_connect/krow_data_connect.dart'; +import 'package:firebase_auth/firebase_auth.dart' as firebase; import 'package:staff_authentication/src/data/repositories_impl/auth_repository_impl.dart'; import 'package:staff_authentication/src/domain/repositories/auth_repository_interface.dart'; import 'package:staff_authentication/src/domain/usecases/sign_in_with_phone_usecase.dart'; @@ -28,7 +29,10 @@ class StaffAuthenticationModule extends Module { void binds(Injector i) { // Repositories i.addLazySingleton( - () => AuthRepositoryImpl(mock: i.get()), + () => AuthRepositoryImpl( + firebaseAuth: firebase.FirebaseAuth.instance, + dataConnect: ExampleConnector.instance, + ), ); // UseCases