Merge pull request #287 from Oloodi/saveUserSessionAndShowHomeData
Save user session and show home data
This commit is contained in:
@@ -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 {
|
||||
|
||||
226
apps/mobile/apps/staff/android/app/google-services.json
Normal file
226
apps/mobile/apps/staff/android/app/google-services.json
Normal 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"
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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._();
|
||||
}
|
||||
@@ -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,13 +8,13 @@ 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,
|
||||
required firebase.FirebaseAuth firebaseAuth,
|
||||
required dc.ExampleConnector dataConnect,
|
||||
}) : _firebaseAuth = firebaseAuth,
|
||||
_dataConnect = dataConnect;
|
||||
|
||||
@@ -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(
|
||||
final createBusinessResponse = await _dataConnect.createBusiness(
|
||||
businessName: companyName,
|
||||
userId: firebaseUser.uid,
|
||||
rateGroup: BusinessRateGroup.STANDARD,
|
||||
status: BusinessStatus.PENDING,
|
||||
)
|
||||
.execute();
|
||||
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<void> 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<domain.User> 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<domain.User> _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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ClientHomeBloc, ClientHomeState>(
|
||||
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,8 +149,15 @@ class ClientHomePage extends StatelessWidget {
|
||||
),
|
||||
child: CircleAvatar(
|
||||
backgroundColor: UiColors.primary.withValues(alpha: 0.1),
|
||||
child: Text(
|
||||
'C',
|
||||
backgroundImage:
|
||||
photoUrl != null && photoUrl.isNotEmpty
|
||||
? NetworkImage(photoUrl)
|
||||
: null,
|
||||
child:
|
||||
photoUrl != null && photoUrl.isNotEmpty
|
||||
? null
|
||||
: Text(
|
||||
avatarLetter,
|
||||
style: UiTypography.body2b.copyWith(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -79,6 +79,30 @@ class HubRepositoryImpl implements HubRepositoryInterface {
|
||||
}
|
||||
|
||||
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;
|
||||
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<String> _getOrCreateTeamId(
|
||||
|
||||
@@ -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,9 +49,17 @@ class SettingsProfileHeader extends StatelessWidget {
|
||||
border: Border.all(color: UiColors.border, width: 2),
|
||||
color: UiColors.white,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'C',
|
||||
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,
|
||||
),
|
||||
@@ -53,7 +70,7 @@ class SettingsProfileHeader extends StatelessWidget {
|
||||
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,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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<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.
|
||||
@override
|
||||
Future<String?> signInWithPhone({required String phoneNumber}) {
|
||||
return mock.signInWithPhone(phoneNumber);
|
||||
Future<String?> signInWithPhone({required String phoneNumber}) async {
|
||||
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.
|
||||
@override
|
||||
Future<void> signOut() {
|
||||
return mock.signOut();
|
||||
return _firebaseAuth.signOut();
|
||||
}
|
||||
|
||||
/// Verifies an OTP code and returns the authenticated user.
|
||||
@override
|
||||
Future<User?> verifyOtp({
|
||||
Future<domain.User?> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<AuthRepositoryInterface>(
|
||||
() => AuthRepositoryImpl(mock: i.get<AuthRepositoryMock>()),
|
||||
() => AuthRepositoryImpl(
|
||||
firebaseAuth: firebase.FirebaseAuth.instance,
|
||||
dataConnect: ExampleConnector.instance,
|
||||
),
|
||||
);
|
||||
|
||||
// UseCases
|
||||
|
||||
Reference in New Issue
Block a user