diff --git a/apps/mobile/packages/core/lib/src/routing/client/route_paths.dart b/apps/mobile/packages/core/lib/src/routing/client/route_paths.dart index f7172e11..900bb545 100644 --- a/apps/mobile/packages/core/lib/src/routing/client/route_paths.dart +++ b/apps/mobile/packages/core/lib/src/routing/client/route_paths.dart @@ -41,6 +41,11 @@ class ClientPaths { /// This serves as the entry point for unauthenticated users. 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. /// /// Supports email/password and social authentication. diff --git a/apps/mobile/packages/core/lib/src/routing/staff/route_paths.dart b/apps/mobile/packages/core/lib/src/routing/staff/route_paths.dart index 54e63c23..1b49991c 100644 --- a/apps/mobile/packages/core/lib/src/routing/staff/route_paths.dart +++ b/apps/mobile/packages/core/lib/src/routing/staff/route_paths.dart @@ -41,6 +41,11 @@ class StaffPaths { /// This serves as the entry point for unauthenticated staff members. 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). /// /// Used for both login and signup flows to verify phone numbers via OTP. diff --git a/apps/mobile/packages/data_connect/lib/src/data_connect_module.dart b/apps/mobile/packages/data_connect/lib/src/data_connect_module.dart index 8f7aa678..5704afb6 100644 --- a/apps/mobile/packages/data_connect/lib/src/data_connect_module.dart +++ b/apps/mobile/packages/data_connect/lib/src/data_connect_module.dart @@ -1,10 +1,10 @@ import 'package:flutter_modular/flutter_modular.dart'; +import 'services/data_connect_service.dart'; /// A module that provides Data Connect dependencies. class DataConnectModule extends Module { @override void exportedBinds(Injector i) { - // No mock bindings anymore. - // Real repositories are instantiated in their feature modules. + i.addInstance(DataConnectService.instance); } } diff --git a/apps/mobile/packages/features/client/authentication/lib/client_authentication.dart b/apps/mobile/packages/features/client/authentication/lib/client_authentication.dart index 1ee73543..5e4eec82 100644 --- a/apps/mobile/packages/features/client/authentication/lib/client_authentication.dart +++ b/apps/mobile/packages/features/client/authentication/lib/client_authentication.dart @@ -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_up_with_email_use_case.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_sign_in_page.dart'; import 'src/presentation/pages/client_sign_up_page.dart'; @@ -54,7 +55,8 @@ class ClientAuthenticationModule extends Module { @override 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.signUp, child: (_) => const ClientSignUpPage()); } diff --git a/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart b/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart index 467a7c07..b64d9f71 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart @@ -414,4 +414,26 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { return domainUser; } + @override + Future 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; + } + } } diff --git a/apps/mobile/packages/features/client/authentication/lib/src/domain/repositories/auth_repository_interface.dart b/apps/mobile/packages/features/client/authentication/lib/src/domain/repositories/auth_repository_interface.dart index 21a1830c..3dbc053f 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/domain/repositories/auth_repository_interface.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/domain/repositories/auth_repository_interface.dart @@ -34,4 +34,7 @@ abstract class AuthRepositoryInterface { /// Terminates the current user session and clears authentication tokens. Future signOut(); + + /// Restores the session if a user is already logged in. + Future restoreSession(); } diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_intro_page.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_intro_page.dart new file mode 100644 index 00000000..f866b43c --- /dev/null +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_intro_page.dart @@ -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 createState() => _ClientIntroPageState(); +} + +class _ClientIntroPageState extends State { + @override + void initState() { + super.initState(); + _checkSession(); + } + + Future _checkSession() async { + // Check session immediately without artificial delay + if (!mounted) return; + + try { + final AuthRepositoryInterface authRepo = Modular.get(); + // 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, + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart b/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart index b247880e..863d815f 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart @@ -257,4 +257,77 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { ); return domainUser; } + @override + Future restoreSession() async { + final User? firebaseUser = _service.auth.currentUser; + if (firebaseUser == null) { + return null; + } + + try { + // 1. Fetch User + final QueryResult 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 + 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; + } + } } diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/domain/repositories/auth_repository_interface.dart b/apps/mobile/packages/features/staff/authentication/lib/src/domain/repositories/auth_repository_interface.dart index 12e05413..e73be91d 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/domain/repositories/auth_repository_interface.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/domain/repositories/auth_repository_interface.dart @@ -20,5 +20,7 @@ abstract interface class AuthRepositoryInterface { /// Signs out the current user. Future signOut(); - // Future 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 restoreSession(); } diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/presentation/pages/intro_page.dart b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/pages/intro_page.dart new file mode 100644 index 00000000..6d27ee1b --- /dev/null +++ b/apps/mobile/packages/features/staff/authentication/lib/src/presentation/pages/intro_page.dart @@ -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 createState() => _IntroPageState(); +} + +class _IntroPageState extends State { + @override + void initState() { + super.initState(); + _checkSession(); + } + + Future _checkSession() async { + // Check session immediately without artificial delay + if (!mounted) return; + + try { + final AuthRepositoryInterface authRepo = Modular.get(); + // 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, + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/authentication/lib/src/staff_authentication_module.dart b/apps/mobile/packages/features/staff/authentication/lib/src/staff_authentication_module.dart index c5380d68..e0426496 100644 --- a/apps/mobile/packages/features/staff/authentication/lib/src/staff_authentication_module.dart +++ b/apps/mobile/packages/features/staff/authentication/lib/src/staff_authentication_module.dart @@ -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/presentation/blocs/auth_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/phone_verification_page.dart'; import 'package:staff_authentication/src/presentation/pages/profile_setup_page.dart'; @@ -54,7 +55,8 @@ class StaffAuthenticationModule extends Module { @override 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( StaffPaths.phoneVerification, child: (BuildContext context) { diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart index 64a112ca..9d799fcb 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart @@ -186,10 +186,20 @@ class ShiftsRepositoryImpl final fdc.QueryResult result = await _service.executeProtected(() => _service.connector .listShiftRolesByVendorId(vendorId: vendorId) .execute()); + + + final allShiftRoles = result.data.shiftRoles; + // Fetch my applications to filter out already booked shifts + final List myShifts = await _fetchApplications(); + final Set myShiftIds = myShifts.map((s) => s.id).toSet(); + final List mappedShifts = []; 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 startDt = _service.toDateTime(sr.startTime); diff --git a/apps/mobile/pubspec.lock b/apps/mobile/pubspec.lock index 25c3fd23..f30d02fc 100644 --- a/apps/mobile/pubspec.lock +++ b/apps/mobile/pubspec.lock @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" charcode: dependency: transitive description: @@ -741,14 +741,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - js: - dependency: transitive - description: - name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.dev" - source: hosted - version: "0.7.2" json_annotation: dependency: transitive description: @@ -817,18 +809,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" melos: dependency: "direct dev" description: @@ -1326,26 +1318,26 @@ packages: dependency: transitive description: name: test - sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" url: "https://pub.dev" source: hosted - version: "1.26.3" + version: "1.29.0" test_api: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" test_core: dependency: transitive description: name: test_core - sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" url: "https://pub.dev" source: hosted - version: "0.6.12" + version: "0.6.15" typed_data: dependency: transitive description: diff --git a/backend/dataconnect/connector/shiftRole/queries.gql b/backend/dataconnect/connector/shiftRole/queries.gql index c1569213..ffba13ae 100644 --- a/backend/dataconnect/connector/shiftRole/queries.gql +++ b/backend/dataconnect/connector/shiftRole/queries.gql @@ -247,6 +247,7 @@ query listShiftRolesByShiftIdAndTimeRange( # ------------------------------------------------------------ query listShiftRolesByVendorId( $vendorId: UUID! + $offset: Int $limit: Int ) @auth(level: USER) { @@ -313,6 +314,7 @@ query listShiftRolesByVendorId( vendor { id companyName } } } + } } diff --git a/backend/dataconnect/schema/ShiftRole.gql b/backend/dataconnect/schema/ShiftRole.gql index 94470ebd..57c742b4 100644 --- a/backend/dataconnect/schema/ShiftRole.gql +++ b/backend/dataconnect/schema/ShiftRole.gql @@ -33,4 +33,6 @@ type ShiftRole @table(name: "shift_roles", key: ["shiftId", "roleId"]) { createdAt: Timestamp @default(expr: "request.time") updatedAt: Timestamp @default(expr: "request.time") + + } diff --git a/internal/api-harness/src/api/krowSDK.js b/internal/api-harness/src/api/krowSDK.js index 7c9ec177..c47392ed 100644 --- a/internal/api-harness/src/api/krowSDK.js +++ b/internal/api-harness/src/api/krowSDK.js @@ -303,14 +303,19 @@ const dataconnectEntityConfig = { Order:{ list: 'listOrder', get: 'getOrderById', - create: 'UpdateOrder', - update: 'updateEnterprise', - delete: 'deleteEnterprise', + create: 'createOrder', + update: 'updateOrder', + delete: 'deleteOrder', filter: 'filterOrder', }, Shift:{ - + list: 'listShifts', + get: 'getShiftById', + create: 'createShift', + update: 'updateShift', + delete: 'deleteShift', + filter: 'filterShifts', } };