Merge pull request #287 from Oloodi/saveUserSessionAndShowHomeData

Save user session and show home data
This commit is contained in:
José Salazar
2026-01-22 18:26:31 -05:00
committed by GitHub
12 changed files with 529 additions and 66 deletions

View File

@@ -3,6 +3,7 @@ plugins {
id("kotlin-android") id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin") id("dev.flutter.flutter-gradle-plugin")
id("com.google.gms.google-services")
} }
android { android {

View File

@@ -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"
}

View File

@@ -21,6 +21,7 @@ plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" 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") include(":app")

View File

@@ -6,10 +6,11 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:staff_authentication/staff_authentication.dart' import 'package:staff_authentication/staff_authentication.dart'
as staff_authentication; as staff_authentication;
import 'package:firebase_core/firebase_core.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(ModularApp(module: AppModule(), child: const AppWidget())); runApp(ModularApp(module: AppModule(), child: const AppWidget()));
} }

View File

@@ -18,6 +18,7 @@ export 'src/mocks/home_repository_mock.dart';
export 'src/mocks/business_repository_mock.dart'; export 'src/mocks/business_repository_mock.dart';
export 'src/mocks/order_repository_mock.dart'; export 'src/mocks/order_repository_mock.dart';
export 'src/data_connect_module.dart'; export 'src/data_connect_module.dart';
export 'src/session/client_session_store.dart';
// Export the generated Data Connect SDK // Export the generated Data Connect SDK
export 'src/dataconnect_generated/generated.dart'; export 'src/dataconnect_generated/generated.dart';

View File

@@ -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._();
}

View File

@@ -1,5 +1,5 @@
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'package:krow_data_connect/krow_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart' as domain; import 'package:krow_domain/krow_domain.dart' as domain;
import '../../domain/repositories/auth_repository_interface.dart'; 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 /// This implementation integrates with Firebase Authentication for user
/// identity management and Krow's Data Connect SDK for storing user profile data. /// identity management and Krow's Data Connect SDK for storing user profile data.
class AuthRepositoryImpl implements AuthRepositoryInterface { class AuthRepositoryImpl implements AuthRepositoryInterface {
final FirebaseAuth _firebaseAuth; final firebase.FirebaseAuth _firebaseAuth;
final ExampleConnector _dataConnect; final dc.ExampleConnector _dataConnect;
/// Creates an [AuthRepositoryImpl] with the real dependencies. /// Creates an [AuthRepositoryImpl] with the real dependencies.
AuthRepositoryImpl({ AuthRepositoryImpl({
required FirebaseAuth firebaseAuth, required firebase.FirebaseAuth firebaseAuth,
required ExampleConnector dataConnect, required dc.ExampleConnector dataConnect,
}) : _firebaseAuth = firebaseAuth, }) : _firebaseAuth = firebaseAuth,
_dataConnect = dataConnect; _dataConnect = dataConnect;
@override @override
Future<domain.User> signInWithEmail({ Future<domain.User> signInWithEmail({
@@ -40,7 +40,8 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
); );
//TO-DO: validate that user is business role and has business account //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') { if (e.code == 'invalid-credential' || e.code == 'wrong-password') {
throw Exception('Incorrect email or password.'); throw Exception('Incorrect email or password.');
} else { } else {
@@ -71,25 +72,23 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
// Client-specific business logic: // Client-specific business logic:
// 1. Create a `Business` entity. // 1. Create a `Business` entity.
// 2. Create a `User` entity associated with the business. // 2. Create a `User` entity associated with the business.
final createBusinessResponse = await _dataConnect final createBusinessResponse = await _dataConnect.createBusiness(
.createBusiness( businessName: companyName,
businessName: companyName, userId: firebaseUser.uid,
userId: firebaseUser.uid, rateGroup: dc.BusinessRateGroup.STANDARD,
rateGroup: BusinessRateGroup.STANDARD, status: dc.BusinessStatus.PENDING,
status: BusinessStatus.PENDING, ).execute();
)
.execute();
final businessData = createBusinessResponse.data?.business_insert; final businessData = createBusinessResponse.data?.business_insert;
if (businessData == null) { if (businessData == null) {
await firebaseUser.delete(); // Rollback if business creation fails await firebaseUser.delete(); // Rollback if business creation fails
throw Exception( throw Exception('Business creation failed after Firebase user registration.');
'Business creation failed after Firebase user registration.',
);
} }
final createUserResponse = await _dataConnect final createUserResponse = await _dataConnect.createUser(
.createUser(id: firebaseUser.uid, role: UserBaseRole.USER) id: firebaseUser.uid,
role: dc.UserBaseRole.USER,
)
.email(email) .email(email)
.userRole('BUSINESS') .userRole('BUSINESS')
.execute(); .execute();
@@ -98,16 +97,15 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
if (newUserData == null) { if (newUserData == null) {
await firebaseUser.delete(); // Rollback if user profile creation fails await firebaseUser.delete(); // Rollback if user profile creation fails
// TO-DO: Also delete the created Business if this fails // TO-DO: Also delete the created Business if this fails
throw Exception( throw Exception('User profile creation failed after Firebase user registration.');
'User profile creation failed after Firebase user registration.',
);
} }
return _getUserProfile( return _getUserProfile(
firebaseUserId: firebaseUser.uid, firebaseUserId: firebaseUser.uid,
fallbackEmail: firebaseUser.email ?? email, fallbackEmail: firebaseUser.email ?? email,
); );
} on FirebaseAuthException catch (e) {
} on firebase.FirebaseAuthException catch (e) {
if (e.code == 'weak-password') { if (e.code == 'weak-password') {
throw Exception('The password provided is too weak.'); throw Exception('The password provided is too weak.');
} else if (e.code == 'email-already-in-use') { } else if (e.code == 'email-already-in-use') {
@@ -116,9 +114,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
throw Exception('Sign-up error: ${e.message}'); throw Exception('Sign-up error: ${e.message}');
} }
} catch (e) { } catch (e) {
throw Exception( throw Exception('Failed to sign up and create user data: ${e.toString()}');
'Failed to sign up and create user data: ${e.toString()}',
);
} }
} }
@@ -126,6 +122,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
Future<void> signOut() async { Future<void> signOut() async {
try { try {
await _firebaseAuth.signOut(); await _firebaseAuth.signOut();
dc.ClientSessionStore.instance.clear();
} catch (e) { } catch (e) {
throw Exception('Error signing out: ${e.toString()}'); throw Exception('Error signing out: ${e.toString()}');
} }
@@ -133,18 +130,14 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
@override @override
Future<domain.User> signInWithSocial({required String provider}) { Future<domain.User> signInWithSocial({required String provider}) {
throw UnimplementedError( throw UnimplementedError('Social authentication with $provider is not yet implemented.');
'Social authentication with $provider is not yet implemented.',
);
} }
Future<domain.User> _getUserProfile({ Future<domain.User> _getUserProfile({
required String firebaseUserId, required String firebaseUserId,
required String? fallbackEmail, required String? fallbackEmail,
}) async { }) async {
final response = await _dataConnect final response = await _dataConnect.getUserById(id: firebaseUserId).execute();
.getUserById(id: firebaseUserId)
.execute();
final user = response.data?.user; final user = response.data?.user;
if (user == null) { if (user == null) {
throw Exception('Authenticated user profile not found in database.'); 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.'); 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;
} }
} }

View File

@@ -3,6 +3,7 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.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_bloc.dart';
import '../blocs/client_home_event.dart'; import '../blocs/client_home_event.dart';
@@ -116,6 +117,14 @@ class ClientHomePage extends StatelessWidget {
Widget _buildHeader(BuildContext context, dynamic i18n) { Widget _buildHeader(BuildContext context, dynamic i18n) {
return BlocBuilder<ClientHomeBloc, ClientHomeState>( return BlocBuilder<ClientHomeBloc, ClientHomeState>(
builder: (context, state) { 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( return Padding(
padding: const EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(
UiConstants.space4, UiConstants.space4,
@@ -140,12 +149,19 @@ class ClientHomePage extends StatelessWidget {
), ),
child: CircleAvatar( child: CircleAvatar(
backgroundColor: UiColors.primary.withValues(alpha: 0.1), backgroundColor: UiColors.primary.withValues(alpha: 0.1),
child: Text( backgroundImage:
'C', photoUrl != null && photoUrl.isNotEmpty
style: UiTypography.body2b.copyWith( ? NetworkImage(photoUrl)
color: UiColors.primary, : null,
), child:
), photoUrl != null && photoUrl.isNotEmpty
? null
: Text(
avatarLetter,
style: UiTypography.body2b.copyWith(
color: UiColors.primary,
),
),
), ),
), ),
const SizedBox(width: UiConstants.space3), const SizedBox(width: UiConstants.space3),
@@ -156,7 +172,7 @@ class ClientHomePage extends StatelessWidget {
i18n.dashboard.welcome_back, i18n.dashboard.welcome_back,
style: UiTypography.footnote2r.textSecondary, style: UiTypography.footnote2r.textSecondary,
), ),
Text('Your Company', style: UiTypography.body1b), Text(businessName, style: UiTypography.body1b),
], ],
), ),
], ],

View File

@@ -79,6 +79,30 @@ class HubRepositoryImpl implements HubRepositoryInterface {
} }
Future<dc.GetBusinessesByUserIdBusinesses> _getBusinessForCurrentUser() async { Future<dc.GetBusinessesByUserIdBusinesses> _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; final user = _firebaseAuth.currentUser;
if (user == null) { if (user == null) {
throw Exception('User is not authenticated.'); 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.'); 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<String> _getOrCreateTeamId( Future<String> _getOrCreateTeamId(

View File

@@ -2,6 +2,7 @@ import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.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. /// A widget that displays the profile header with avatar and company info.
class SettingsProfileHeader extends StatelessWidget { class SettingsProfileHeader extends StatelessWidget {
@@ -12,6 +13,14 @@ class SettingsProfileHeader extends StatelessWidget {
/// Builds the profile header UI. /// Builds the profile header UI.
Widget build(BuildContext context) { Widget build(BuildContext context) {
final labels = t.client_settings.profile; 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( return SliverAppBar(
backgroundColor: UiColors.bgSecondary, backgroundColor: UiColors.bgSecondary,
@@ -40,20 +49,28 @@ class SettingsProfileHeader extends StatelessWidget {
border: Border.all(color: UiColors.border, width: 2), border: Border.all(color: UiColors.border, width: 2),
color: UiColors.white, color: UiColors.white,
), ),
child: Center( child: CircleAvatar(
child: Text( backgroundColor: UiColors.primary.withValues(alpha: 0.1),
'C', backgroundImage:
style: UiTypography.headline1m.copyWith( photoUrl != null && photoUrl.isNotEmpty
color: UiColors.primary, ? NetworkImage(photoUrl)
), : null,
), child:
photoUrl != null && photoUrl.isNotEmpty
? null
: Text(
avatarLetter,
style: UiTypography.headline1m.copyWith(
color: UiColors.primary,
),
),
), ),
), ),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text('Your Company', style: UiTypography.body1b.textPrimary), Text(businessName, style: UiTypography.body1b.textPrimary),
const SizedBox(height: UiConstants.space1), const SizedBox(height: UiConstants.space1),
Row( Row(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
@@ -65,7 +82,7 @@ class SettingsProfileHeader extends StatelessWidget {
color: UiColors.textSecondary, color: UiColors.textSecondary,
), ),
Text( Text(
'client@example.com', email,
style: UiTypography.footnote1r.textSecondary, style: UiTypography.footnote1r.textSecondary,
), ),
], ],

View File

@@ -1,34 +1,116 @@
import 'package:krow_data_connect/krow_data_connect.dart'; import 'dart:async';
import 'package:krow_domain/krow_domain.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'; import '../../domain/repositories/auth_repository_interface.dart';
/// Implementation of [AuthRepositoryInterface]. /// Implementation of [AuthRepositoryInterface].
class AuthRepositoryImpl implements 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 @override
Stream<User?> get currentUser => mock.currentUser; Stream<domain.User?> 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. /// Signs in with a phone number and returns a verification ID.
@override @override
Future<String?> signInWithPhone({required String phoneNumber}) { Future<String?> signInWithPhone({required String phoneNumber}) async {
return mock.signInWithPhone(phoneNumber); final completer = Completer<String?>();
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. /// Signs out the current user.
@override @override
Future<void> signOut() { Future<void> signOut() {
return mock.signOut(); return _firebaseAuth.signOut();
} }
/// Verifies an OTP code and returns the authenticated user. /// Verifies an OTP code and returns the authenticated user.
@override @override
Future<User?> verifyOtp({ Future<domain.User?> verifyOtp({
required String verificationId, required String verificationId,
required String smsCode, required String smsCode,
}) { }) async {
return mock.verifyOtp(verificationId, smsCode); 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,
);
} }
} }

View File

@@ -2,6 +2,7 @@ library staff_authentication;
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_data_connect/krow_data_connect.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/data/repositories_impl/auth_repository_impl.dart';
import 'package:staff_authentication/src/domain/repositories/auth_repository_interface.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'; 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) { void binds(Injector i) {
// Repositories // Repositories
i.addLazySingleton<AuthRepositoryInterface>( i.addLazySingleton<AuthRepositoryInterface>(
() => AuthRepositoryImpl(mock: i.get<AuthRepositoryMock>()), () => AuthRepositoryImpl(
firebaseAuth: firebase.FirebaseAuth.instance,
dataConnect: ExampleConnector.instance,
),
); );
// UseCases // UseCases