diff --git a/apps/mobile/apps/staff/lib/main.dart b/apps/mobile/apps/staff/lib/main.dart index d127d3e1..2716abc4 100644 --- a/apps/mobile/apps/staff/lib/main.dart +++ b/apps/mobile/apps/staff/lib/main.dart @@ -12,6 +12,7 @@ import 'package:staff_authentication/staff_authentication.dart' import 'package:staff_main/staff_main.dart' as staff_main; import 'package:krow_core/core.dart'; +import 'package:image_picker/image_picker.dart'; import 'src/widgets/session_listener.dart'; void main() async { @@ -26,7 +27,10 @@ void main() async { // Initialize session listener for Firebase Auth state changes DataConnectService.instance.initializeAuthListener( - allowedRoles: ['STAFF', 'BOTH'], // Only allow users with STAFF or BOTH roles + allowedRoles: [ + 'STAFF', + 'BOTH', + ], // Only allow users with STAFF or BOTH roles ); runApp( @@ -40,11 +44,22 @@ void main() async { /// The main application module. class AppModule extends Module { @override - List get imports => - [ - core_localization.LocalizationModule(), - staff_authentication.StaffAuthenticationModule(), - ]; + void binds(Injector i) { + i.addLazySingleton(ImagePicker.new); + i.addLazySingleton( + () => CameraService(i.get()), + ); + i.addLazySingleton( + () => GalleryService(i.get()), + ); + i.addLazySingleton(FilePickerService.new); + } + + @override + List get imports => [ + core_localization.LocalizationModule(), + staff_authentication.StaffAuthenticationModule(), + ]; @override void routes(RouteManager r) { diff --git a/apps/mobile/packages/core/lib/src/services/device/camera/camera_service.dart b/apps/mobile/packages/core/lib/src/services/device/camera/camera_service.dart index fd78b306..c7317aa4 100644 --- a/apps/mobile/packages/core/lib/src/services/device/camera/camera_service.dart +++ b/apps/mobile/packages/core/lib/src/services/device/camera/camera_service.dart @@ -4,7 +4,7 @@ import 'package:krow_domain/krow_domain.dart'; /// Service for capturing photos and videos using the device camera. class CameraService extends BaseDeviceService { /// Creates a [CameraService]. - CameraService(this._picker); + CameraService(ImagePicker picker) : _picker = picker; final ImagePicker _picker; diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/data/repositories_impl/attire_repository_impl.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/data/repositories_impl/attire_repository_impl.dart index 727c8f77..21b00a93 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/data/repositories_impl/attire_repository_impl.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/data/repositories_impl/attire_repository_impl.dart @@ -31,7 +31,7 @@ class AttireRepositoryImpl implements AttireRepository { } @override - Future uploadPhoto(String itemId) async { + Future uploadPhoto(String itemId, String filePath) async { // In a real app, this would upload to Firebase Storage first. // Since the prototype returns a mock URL, we'll use that to upsert our record. final String mockUrl = 'mock_url_for_$itemId'; diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/domain/arguments/upload_attire_photo_arguments.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/domain/arguments/upload_attire_photo_arguments.dart index 1745879c..dafdac1f 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/domain/arguments/upload_attire_photo_arguments.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/domain/arguments/upload_attire_photo_arguments.dart @@ -7,10 +7,17 @@ class UploadAttirePhotoArguments extends UseCaseArgument { // We'll stick to that signature for now to "preserve behavior". /// Creates a [UploadAttirePhotoArguments]. - const UploadAttirePhotoArguments({required this.itemId}); + const UploadAttirePhotoArguments({ + required this.itemId, + required this.filePath, + }); + /// The ID of the attire item being uploaded. final String itemId; + /// The local path to the photo file. + final String filePath; + @override - List get props => [itemId]; + List get props => [itemId, filePath]; } diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/domain/repositories/attire_repository.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/domain/repositories/attire_repository.dart index 1b4742ad..a0452704 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/domain/repositories/attire_repository.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/domain/repositories/attire_repository.dart @@ -4,8 +4,8 @@ abstract interface class AttireRepository { /// Fetches the list of available attire options. Future> getAttireOptions(); - /// Simulates uploading a photo for a specific attire item. - Future uploadPhoto(String itemId); + /// Uploads a photo for a specific attire item. + Future uploadPhoto(String itemId, String filePath); /// Saves the user's attire selection and attestations. Future saveAttire({ diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/domain/usecases/upload_attire_photo_usecase.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/domain/usecases/upload_attire_photo_usecase.dart index 7c6de30a..d76edf06 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/domain/usecases/upload_attire_photo_usecase.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/domain/usecases/upload_attire_photo_usecase.dart @@ -3,14 +3,14 @@ import '../arguments/upload_attire_photo_arguments.dart'; import '../repositories/attire_repository.dart'; /// Use case to upload a photo for an attire item. -class UploadAttirePhotoUseCase extends UseCase { - +class UploadAttirePhotoUseCase + extends UseCase { /// Creates a [UploadAttirePhotoUseCase]. UploadAttirePhotoUseCase(this._repository); final AttireRepository _repository; @override Future call(UploadAttirePhotoArguments arguments) { - return _repository.uploadPhoto(arguments.itemId); + return _repository.uploadPhoto(arguments.itemId, arguments.filePath); } } diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/blocs/attire_capture/attire_capture_cubit.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/blocs/attire_capture/attire_capture_cubit.dart index 884abb37..cad159e0 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/blocs/attire_capture/attire_capture_cubit.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/blocs/attire_capture/attire_capture_cubit.dart @@ -16,14 +16,14 @@ class AttireCaptureCubit extends Cubit emit(state.copyWith(isAttested: value)); } - Future uploadPhoto(String itemId) async { + Future uploadPhoto(String itemId, String filePath) async { emit(state.copyWith(status: AttireCaptureStatus.uploading)); await handleError( emit: emit, action: () async { final String url = await _uploadAttirePhotoUseCase( - UploadAttirePhotoArguments(itemId: itemId), + UploadAttirePhotoArguments(itemId: itemId, filePath: filePath), ); emit( diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_capture_page.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_capture_page.dart index f36fbef6..9e6e55e4 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_capture_page.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/pages/attire_capture_page.dart @@ -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_core/core.dart'; import 'package:krow_domain/krow_domain.dart'; import 'package:staff_attire/src/presentation/blocs/attire_capture/attire_capture_cubit.dart'; import 'package:staff_attire/src/presentation/blocs/attire_capture/attire_capture_state.dart'; @@ -27,21 +28,71 @@ class AttireCapturePage extends StatefulWidget { } class _AttireCapturePageState extends State { - void _onUpload(BuildContext context) { + /// On gallery button press + Future _onGallery(BuildContext context) async { final AttireCaptureCubit cubit = BlocProvider.of( context, ); + if (!cubit.state.isAttested) { - UiSnackbar.show( - context, - message: 'Please attest that you own this item.', - type: UiSnackbarType.error, - margin: const EdgeInsets.all(UiConstants.space4), - ); + _showAttestationWarning(context); return; } - // Call the upload via cubit - cubit.uploadPhoto(widget.item.id); + + try { + final GalleryService service = Modular.get(); + final String? path = await service.pickImage(); + if (path != null && context.mounted) { + await cubit.uploadPhoto(widget.item.id, path); + } + } catch (e) { + if (context.mounted) { + _showError(context, 'Could not access gallery: $e'); + } + } + } + + /// On camera button press + Future _onCamera(BuildContext context) async { + final AttireCaptureCubit cubit = BlocProvider.of( + context, + ); + + if (!cubit.state.isAttested) { + _showAttestationWarning(context); + return; + } + + try { + final CameraService service = Modular.get(); + final String? path = await service.takePhoto(); + if (path != null && context.mounted) { + await cubit.uploadPhoto(widget.item.id, path); + } + } catch (e) { + if (context.mounted) { + _showError(context, 'Could not access camera: $e'); + } + } + } + + void _showAttestationWarning(BuildContext context) { + UiSnackbar.show( + context, + message: 'Please attest that you own this item.', + type: UiSnackbarType.error, + margin: const EdgeInsets.all(UiConstants.space4), + ); + } + + void _showError(BuildContext context, String message) { + debugPrint(message); + UiSnackbar.show( + context, + message: 'Could not access camera or gallery. Please try again.', + type: UiSnackbarType.error, + margin: const EdgeInsets.all(UiConstants.space4), + ); } @override @@ -174,7 +225,10 @@ class _AttireCapturePageState extends State { ), ) else - AttireUploadButtons(onUpload: _onUpload), + AttireUploadButtons( + onGallery: () => _onGallery(context), + onCamera: () => _onCamera(context), + ), ], ), ), diff --git a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/attire_upload_buttons.dart b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/attire_upload_buttons.dart index 83067e7e..e6bcb712 100644 --- a/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/attire_upload_buttons.dart +++ b/apps/mobile/packages/features/staff/profile_sections/onboarding/attire/lib/src/presentation/widgets/attire_capture_page/attire_upload_buttons.dart @@ -2,9 +2,14 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; class AttireUploadButtons extends StatelessWidget { - const AttireUploadButtons({super.key, required this.onUpload}); + const AttireUploadButtons({ + super.key, + required this.onGallery, + required this.onCamera, + }); - final void Function(BuildContext) onUpload; + final VoidCallback onGallery; + final VoidCallback onCamera; @override Widget build(BuildContext context) { @@ -14,7 +19,7 @@ class AttireUploadButtons extends StatelessWidget { child: UiButton.secondary( leadingIcon: UiIcons.gallery, text: 'Gallery', - onPressed: () => onUpload(context), + onPressed: onGallery, ), ), const SizedBox(width: UiConstants.space4), @@ -22,7 +27,7 @@ class AttireUploadButtons extends StatelessWidget { child: UiButton.primary( leadingIcon: UiIcons.camera, text: 'Camera', - onPressed: () => onUpload(context), + onPressed: onCamera, ), ), ],