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")
|
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 {
|
||||||
|
|||||||
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("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")
|
||||||
|
|||||||
@@ -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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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: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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user