fix(mobile): resolve client crash and shift status inconsistency

This commit is contained in:
2026-02-17 16:23:10 +05:30
parent 2ebe40a920
commit da8192418f
14 changed files with 261 additions and 5 deletions

View File

@@ -41,6 +41,11 @@ class ClientPaths {
/// This serves as the entry point for unauthenticated users. /// This serves as the entry point for unauthenticated users.
static const String root = '/'; static const String root = '/';
/// Get Started page (relative path within auth module).
///
/// The landing page for unauthenticated users, offering login/signup options.
static const String getStarted = '/get-started';
/// Sign-in page where existing clients can log into their account. /// Sign-in page where existing clients can log into their account.
/// ///
/// Supports email/password and social authentication. /// Supports email/password and social authentication.

View File

@@ -41,6 +41,11 @@ class StaffPaths {
/// This serves as the entry point for unauthenticated staff members. /// This serves as the entry point for unauthenticated staff members.
static const String root = '/'; static const String root = '/';
/// Get Started page (relative path within auth module).
///
/// The landing page for unauthenticated users, offering login/signup options.
static const String getStarted = '/get-started';
/// Phone verification page (relative path within auth module). /// Phone verification page (relative path within auth module).
/// ///
/// Used for both login and signup flows to verify phone numbers via OTP. /// Used for both login and signup flows to verify phone numbers via OTP.

View File

@@ -1,10 +1,10 @@
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'services/data_connect_service.dart';
/// A module that provides Data Connect dependencies. /// A module that provides Data Connect dependencies.
class DataConnectModule extends Module { class DataConnectModule extends Module {
@override @override
void exportedBinds(Injector i) { void exportedBinds(Injector i) {
// No mock bindings anymore. i.addInstance<DataConnectService>(DataConnectService.instance);
// Real repositories are instantiated in their feature modules.
} }
} }

View File

@@ -10,6 +10,7 @@ import 'src/domain/usecases/sign_in_with_social_use_case.dart';
import 'src/domain/usecases/sign_out_use_case.dart'; import 'src/domain/usecases/sign_out_use_case.dart';
import 'src/domain/usecases/sign_up_with_email_use_case.dart'; import 'src/domain/usecases/sign_up_with_email_use_case.dart';
import 'src/presentation/blocs/client_auth_bloc.dart'; import 'src/presentation/blocs/client_auth_bloc.dart';
import 'src/presentation/pages/client_intro_page.dart';
import 'src/presentation/pages/client_get_started_page.dart'; import 'src/presentation/pages/client_get_started_page.dart';
import 'src/presentation/pages/client_sign_in_page.dart'; import 'src/presentation/pages/client_sign_in_page.dart';
import 'src/presentation/pages/client_sign_up_page.dart'; import 'src/presentation/pages/client_sign_up_page.dart';
@@ -54,7 +55,8 @@ class ClientAuthenticationModule extends Module {
@override @override
void routes(RouteManager r) { void routes(RouteManager r) {
r.child(ClientPaths.root, child: (_) => const ClientGetStartedPage()); r.child(ClientPaths.root, child: (_) => const ClientIntroPage());
r.child(ClientPaths.getStarted, child: (_) => const ClientGetStartedPage());
r.child(ClientPaths.signIn, child: (_) => const ClientSignInPage()); r.child(ClientPaths.signIn, child: (_) => const ClientSignInPage());
r.child(ClientPaths.signUp, child: (_) => const ClientSignUpPage()); r.child(ClientPaths.signUp, child: (_) => const ClientSignUpPage());
} }

View File

@@ -414,4 +414,26 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
return domainUser; return domainUser;
} }
@override
Future<domain.User?> restoreSession() async {
final firebase.User? firebaseUser = _service.auth.currentUser;
if (firebaseUser == null) {
return null;
}
try {
return await _getUserProfile(
firebaseUserId: firebaseUser.uid,
fallbackEmail: firebaseUser.email,
requireBusinessRole: true,
);
} catch (e) {
// If the user is not found or other permanent errors, we should probably sign out
if (e is UserNotFoundException || e is UnauthorizedAppException) {
await _service.auth.signOut();
return null;
}
rethrow;
}
}
} }

View File

@@ -34,4 +34,7 @@ abstract class AuthRepositoryInterface {
/// Terminates the current user session and clears authentication tokens. /// Terminates the current user session and clears authentication tokens.
Future<void> signOut(); Future<void> signOut();
/// Restores the session if a user is already logged in.
Future<User?> restoreSession();
} }

View File

@@ -0,0 +1,63 @@
import 'dart:async';
import 'package:client_authentication/src/domain/repositories/auth_repository_interface.dart';
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
class ClientIntroPage extends StatefulWidget {
const ClientIntroPage({super.key});
@override
State<ClientIntroPage> createState() => _ClientIntroPageState();
}
class _ClientIntroPageState extends State<ClientIntroPage> {
@override
void initState() {
super.initState();
_checkSession();
}
Future<void> _checkSession() async {
// Check session immediately without artificial delay
if (!mounted) return;
try {
final AuthRepositoryInterface authRepo = Modular.get<AuthRepositoryInterface>();
// Add a timeout to prevent infinite loading
final user = await authRepo.restoreSession().timeout(
const Duration(seconds: 5),
onTimeout: () {
throw TimeoutException('Session restore timed out');
},
);
if (mounted) {
if (user != null) {
Modular.to.navigate(ClientPaths.home);
} else {
Modular.to.navigate(ClientPaths.getStarted);
}
}
} catch (e) {
debugPrint('ClientIntroPage: Session check error: $e');
if (mounted) {
Modular.to.navigate(ClientPaths.getStarted);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
body: Center(
child: Image.asset(
'assets/logo-blue.png',
package: 'design_system',
width: 120,
),
),
);
}
}

View File

@@ -257,4 +257,77 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
); );
return domainUser; return domainUser;
} }
@override
Future<domain.User?> restoreSession() async {
final User? firebaseUser = _service.auth.currentUser;
if (firebaseUser == null) {
return null;
}
try {
// 1. Fetch User
final QueryResult<GetUserByIdData, GetUserByIdVariables> response =
await _service.run(() => _service.connector
.getUserById(
id: firebaseUser.uid,
)
.execute());
final GetUserByIdUser? user = response.data.user;
if (user == null) {
return null;
}
// 2. Check Role
if (user.userRole != 'STAFF' && user.userRole != 'BOTH') {
return null;
}
// 3. Fetch Staff Profile
final QueryResult<GetStaffByUserIdData, GetStaffByUserIdVariables>
staffResponse = await _service.run(() => _service.connector
.getStaffByUserId(
userId: firebaseUser.uid,
)
.execute());
if (staffResponse.data.staffs.isEmpty) {
return null;
}
final GetStaffByUserIdStaffs staffRecord = staffResponse.data.staffs.first;
// 4. Populate Session
final domain.User domainUser = domain.User(
id: firebaseUser.uid,
email: user.email ?? '',
phone: firebaseUser.phoneNumber,
role: user.role.stringValue,
);
final domain.Staff domainStaff = domain.Staff(
id: staffRecord.id,
authProviderId: staffRecord.userId,
name: staffRecord.fullName,
email: staffRecord.email ?? '',
phone: staffRecord.phone,
status: domain.StaffStatus.completedProfile,
address: staffRecord.addres,
avatar: staffRecord.photoUrl,
);
StaffSessionStore.instance.setSession(
StaffSession(
user: domainUser,
staff: domainStaff,
ownerId: staffRecord.ownerId,
),
);
return domainUser;
} catch (e) {
// If restoration fails (network, etc), we rethrow to let UI handle it.
rethrow;
}
}
} }

View File

@@ -20,5 +20,7 @@ abstract interface class AuthRepositoryInterface {
/// Signs out the current user. /// Signs out the current user.
Future<void> signOut(); Future<void> signOut();
// Future<Staff?> getStaffProfile(String userId); // Could be moved to a separate repository if needed, but useful here for routing logic.
/// Restores the session if a user is already logged in.
Future<User?> restoreSession();
} }

View File

@@ -0,0 +1,65 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import 'package:staff_authentication/src/domain/repositories/auth_repository_interface.dart';
class IntroPage extends StatefulWidget {
const IntroPage({super.key});
@override
State<IntroPage> createState() => _IntroPageState();
}
class _IntroPageState extends State<IntroPage> {
@override
void initState() {
super.initState();
_checkSession();
}
Future<void> _checkSession() async {
// Check session immediately without artificial delay
if (!mounted) return;
try {
final AuthRepositoryInterface authRepo = Modular.get<AuthRepositoryInterface>();
// Add a timeout to prevent infinite loading
final user = await authRepo.restoreSession().timeout(
const Duration(seconds: 5),
onTimeout: () {
// If it takes too long, navigate to Get Started.
// This handles poor network conditions gracefully.
throw TimeoutException('Session restore timed out');
},
);
if (mounted) {
if (user != null) {
Modular.to.navigate(StaffPaths.home);
} else {
Modular.to.navigate(StaffPaths.getStarted);
}
}
} catch (e) {
debugPrint('IntroPage: Session check error: $e');
if (mounted) {
Modular.to.navigate(StaffPaths.getStarted);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surface,
body: Center(
child: Image.asset(
'assets/logo-yellow.png',
package: 'design_system',
width: 120,
),
),
);
}
}

View File

@@ -14,6 +14,7 @@ import 'package:staff_authentication/src/data/repositories_impl/place_repository
import 'package:staff_authentication/src/domain/usecases/search_cities_usecase.dart'; import 'package:staff_authentication/src/domain/usecases/search_cities_usecase.dart';
import 'package:staff_authentication/src/presentation/blocs/auth_bloc.dart'; import 'package:staff_authentication/src/presentation/blocs/auth_bloc.dart';
import 'package:staff_authentication/src/presentation/blocs/profile_setup/profile_setup_bloc.dart'; import 'package:staff_authentication/src/presentation/blocs/profile_setup/profile_setup_bloc.dart';
import 'package:staff_authentication/src/presentation/pages/intro_page.dart';
import 'package:staff_authentication/src/presentation/pages/get_started_page.dart'; import 'package:staff_authentication/src/presentation/pages/get_started_page.dart';
import 'package:staff_authentication/src/presentation/pages/phone_verification_page.dart'; import 'package:staff_authentication/src/presentation/pages/phone_verification_page.dart';
import 'package:staff_authentication/src/presentation/pages/profile_setup_page.dart'; import 'package:staff_authentication/src/presentation/pages/profile_setup_page.dart';
@@ -54,7 +55,8 @@ class StaffAuthenticationModule extends Module {
@override @override
void routes(RouteManager r) { void routes(RouteManager r) {
r.child(StaffPaths.root, child: (_) => const GetStartedPage()); r.child(StaffPaths.root, child: (_) => const IntroPage());
r.child(StaffPaths.getStarted, child: (_) => const GetStartedPage());
r.child( r.child(
StaffPaths.phoneVerification, StaffPaths.phoneVerification,
child: (BuildContext context) { child: (BuildContext context) {

View File

@@ -186,10 +186,20 @@ class ShiftsRepositoryImpl
final fdc.QueryResult<dc.ListShiftRolesByVendorIdData, dc.ListShiftRolesByVendorIdVariables> result = await _service.executeProtected(() => _service.connector final fdc.QueryResult<dc.ListShiftRolesByVendorIdData, dc.ListShiftRolesByVendorIdVariables> result = await _service.executeProtected(() => _service.connector
.listShiftRolesByVendorId(vendorId: vendorId) .listShiftRolesByVendorId(vendorId: vendorId)
.execute()); .execute());
final allShiftRoles = result.data.shiftRoles; final allShiftRoles = result.data.shiftRoles;
// Fetch my applications to filter out already booked shifts
final List<Shift> myShifts = await _fetchApplications();
final Set<String> myShiftIds = myShifts.map((s) => s.id).toSet();
final List<Shift> mappedShifts = []; final List<Shift> mappedShifts = [];
for (final sr in allShiftRoles) { for (final sr in allShiftRoles) {
// Skip if I have already applied/booked this shift
if (myShiftIds.contains(sr.shiftId)) continue;
final DateTime? shiftDate = _service.toDateTime(sr.shift.date); final DateTime? shiftDate = _service.toDateTime(sr.shift.date);
final startDt = _service.toDateTime(sr.startTime); final startDt = _service.toDateTime(sr.startTime);

View File

@@ -247,6 +247,7 @@ query listShiftRolesByShiftIdAndTimeRange(
# ------------------------------------------------------------ # ------------------------------------------------------------
query listShiftRolesByVendorId( query listShiftRolesByVendorId(
$vendorId: UUID! $vendorId: UUID!
$offset: Int $offset: Int
$limit: Int $limit: Int
) @auth(level: USER) { ) @auth(level: USER) {
@@ -313,6 +314,7 @@ query listShiftRolesByVendorId(
vendor { id companyName } vendor { id companyName }
} }
} }
} }
} }

View File

@@ -33,4 +33,6 @@ type ShiftRole @table(name: "shift_roles", key: ["shiftId", "roleId"]) {
createdAt: Timestamp @default(expr: "request.time") createdAt: Timestamp @default(expr: "request.time")
updatedAt: Timestamp @default(expr: "request.time") updatedAt: Timestamp @default(expr: "request.time")
} }