feat: Implement attire photo capture, update AttireItem entity, and streamline the photo upload and state management flow.
This commit is contained in:
@@ -5,23 +5,23 @@ import 'package:design_system/design_system.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:marionette_flutter/marionette_flutter.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krowwithus_staff/firebase_options.dart';
|
||||
import 'package:marionette_flutter/marionette_flutter.dart';
|
||||
import 'package:staff_authentication/staff_authentication.dart'
|
||||
as staff_authentication;
|
||||
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 {
|
||||
final bool isFlutterTest =
|
||||
!kIsWeb ? Platform.environment.containsKey('FLUTTER_TEST') : false;
|
||||
final bool isFlutterTest = !kIsWeb
|
||||
? Platform.environment.containsKey('FLUTTER_TEST')
|
||||
: false;
|
||||
if (kDebugMode && !isFlutterTest) {
|
||||
MarionetteBinding.ensureInitialized(
|
||||
MarionetteConfiguration(
|
||||
@@ -63,20 +63,9 @@ void main() async {
|
||||
|
||||
/// The main application module.
|
||||
class AppModule extends Module {
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
i.addLazySingleton<ImagePicker>(ImagePicker.new);
|
||||
i.addLazySingleton<CameraService>(
|
||||
() => CameraService(i.get<ImagePicker>()),
|
||||
);
|
||||
i.addLazySingleton<GalleryService>(
|
||||
() => GalleryService(i.get<ImagePicker>()),
|
||||
);
|
||||
i.addLazySingleton<FilePickerService>(FilePickerService.new);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Module> get imports => <Module>[
|
||||
CoreModule(),
|
||||
core_localization.LocalizationModule(),
|
||||
staff_authentication.StaffAuthenticationModule(),
|
||||
];
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
library;
|
||||
|
||||
export 'src/core_module.dart';
|
||||
|
||||
export 'src/domain/arguments/usecase_argument.dart';
|
||||
export 'src/domain/usecases/usecase.dart';
|
||||
export 'src/utils/date_time_utils.dart';
|
||||
|
||||
48
apps/mobile/packages/core/lib/src/core_module.dart
Normal file
48
apps/mobile/packages/core/lib/src/core_module.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import '../core.dart';
|
||||
|
||||
/// A module that provides core services and shared dependencies.
|
||||
///
|
||||
/// This module should be imported by the root [AppModule] to make
|
||||
/// core services available globally as singletons.
|
||||
class CoreModule extends Module {
|
||||
@override
|
||||
void exportedBinds(Injector i) {
|
||||
// 1. Register the base HTTP client
|
||||
i.addSingleton<Dio>(() => Dio());
|
||||
|
||||
// 2. Register the base API service
|
||||
i.addSingleton<BaseApiService>(() => ApiService(i.get<Dio>()));
|
||||
|
||||
// 3. Register Core API Services (Orchestrators)
|
||||
i.addSingleton<FileUploadService>(
|
||||
() => FileUploadService(i.get<BaseApiService>()),
|
||||
);
|
||||
i.addSingleton<SignedUrlService>(
|
||||
() => SignedUrlService(i.get<BaseApiService>()),
|
||||
);
|
||||
i.addSingleton<VerificationService>(
|
||||
() => VerificationService(i.get<BaseApiService>()),
|
||||
);
|
||||
i.addSingleton<LlmService>(() => LlmService(i.get<BaseApiService>()));
|
||||
|
||||
// 4. Register Device dependency
|
||||
i.addSingleton<ImagePicker>(ImagePicker.new);
|
||||
|
||||
// 5. Register Device Services
|
||||
i.addSingleton<CameraService>(() => CameraService(i.get<ImagePicker>()));
|
||||
i.addSingleton<GalleryService>(() => GalleryService(i.get<ImagePicker>()));
|
||||
i.addSingleton<FilePickerService>(FilePickerService.new);
|
||||
i.addSingleton<DeviceFileUploadService>(
|
||||
() => DeviceFileUploadService(
|
||||
cameraService: i.get<CameraService>(),
|
||||
galleryService: i.get<GalleryService>(),
|
||||
apiUploadService: i.get<FileUploadService>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,24 +8,26 @@ class CoreApiEndpoints {
|
||||
static const String baseUrl = AppConfig.coreApiBaseUrl;
|
||||
|
||||
/// Upload a file.
|
||||
static const String uploadFile = '/core/upload-file';
|
||||
static const String uploadFile = '$baseUrl/core/upload-file';
|
||||
|
||||
/// Create a signed URL for a file.
|
||||
static const String createSignedUrl = '/core/create-signed-url';
|
||||
static const String createSignedUrl = '$baseUrl/core/create-signed-url';
|
||||
|
||||
/// Invoke a Large Language Model.
|
||||
static const String invokeLlm = '/core/invoke-llm';
|
||||
static const String invokeLlm = '$baseUrl/core/invoke-llm';
|
||||
|
||||
/// Root for verification operations.
|
||||
static const String verifications = '/core/verifications';
|
||||
static const String verifications = '$baseUrl/core/verifications';
|
||||
|
||||
/// Get status of a verification job.
|
||||
static String verificationStatus(String id) => '/core/verifications/$id';
|
||||
static String verificationStatus(String id) =>
|
||||
'$baseUrl/core/verifications/$id';
|
||||
|
||||
/// Review a verification decision.
|
||||
static String verificationReview(String id) =>
|
||||
'/core/verifications/$id/review';
|
||||
'$baseUrl/core/verifications/$id/review';
|
||||
|
||||
/// Retry a verification job.
|
||||
static String verificationRetry(String id) => '/core/verifications/$id/retry';
|
||||
static String verificationRetry(String id) =>
|
||||
'$baseUrl/core/verifications/$id/retry';
|
||||
}
|
||||
|
||||
@@ -229,7 +229,8 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
||||
return optionsResponse.data.attireOptions.map((e) {
|
||||
final GetStaffAttireStaffAttires? userAttire = attireMap[e.id];
|
||||
return AttireItem(
|
||||
id: e.itemId,
|
||||
id: e.id,
|
||||
code: e.itemId,
|
||||
label: e.label,
|
||||
description: e.description,
|
||||
imageUrl: e.imageUrl,
|
||||
@@ -238,6 +239,7 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
||||
userAttire?.verificationStatus?.stringValue,
|
||||
),
|
||||
photoUrl: userAttire?.verificationPhotoUrl,
|
||||
verificationId: userAttire?.verificationId,
|
||||
);
|
||||
}).toList();
|
||||
});
|
||||
@@ -263,7 +265,7 @@ class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
|
||||
await _service.connector
|
||||
.upsertStaffAttire(staffId: staffId, attireOptionId: attireOptionId)
|
||||
.verificationPhotoUrl(photoUrl)
|
||||
// .verificationId(verificationId) // Uncomment after SDK regeneration
|
||||
.verificationId(verificationId)
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ class AttireItem extends Equatable {
|
||||
/// Creates an [AttireItem].
|
||||
const AttireItem({
|
||||
required this.id,
|
||||
required this.code,
|
||||
required this.label,
|
||||
this.description,
|
||||
this.imageUrl,
|
||||
@@ -18,9 +19,12 @@ class AttireItem extends Equatable {
|
||||
this.verificationId,
|
||||
});
|
||||
|
||||
/// Unique identifier of the attire item.
|
||||
/// Unique identifier of the attire item (UUID).
|
||||
final String id;
|
||||
|
||||
/// String code for the attire item (e.g. BLACK_TSHIRT).
|
||||
final String code;
|
||||
|
||||
/// Display name of the item.
|
||||
final String label;
|
||||
|
||||
@@ -45,6 +49,7 @@ class AttireItem extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
id,
|
||||
code,
|
||||
label,
|
||||
description,
|
||||
imageUrl,
|
||||
@@ -53,4 +58,29 @@ class AttireItem extends Equatable {
|
||||
photoUrl,
|
||||
verificationId,
|
||||
];
|
||||
|
||||
/// Creates a copy of this [AttireItem] with the given fields replaced.
|
||||
AttireItem copyWith({
|
||||
String? id,
|
||||
String? code,
|
||||
String? label,
|
||||
String? description,
|
||||
String? imageUrl,
|
||||
bool? isMandatory,
|
||||
AttireVerificationStatus? verificationStatus,
|
||||
String? photoUrl,
|
||||
String? verificationId,
|
||||
}) {
|
||||
return AttireItem(
|
||||
id: id ?? this.id,
|
||||
code: code ?? this.code,
|
||||
label: label ?? this.label,
|
||||
description: description ?? this.description,
|
||||
imageUrl: imageUrl ?? this.imageUrl,
|
||||
isMandatory: isMandatory ?? this.isMandatory,
|
||||
verificationStatus: verificationStatus ?? this.verificationStatus,
|
||||
photoUrl: photoUrl ?? this.photoUrl,
|
||||
verificationId: verificationId ?? this.verificationId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:staff_attire/src/presentation/blocs/attire/attire_cubit.dart';
|
||||
import 'package:staff_attire/src/presentation/blocs/attire_capture/attire_capture_cubit.dart';
|
||||
@@ -13,6 +14,14 @@ import 'presentation/pages/attire_page.dart';
|
||||
class StaffAttireModule extends Module {
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
/// third party services
|
||||
i.addLazySingleton<ImagePicker>(ImagePicker.new);
|
||||
|
||||
/// local services
|
||||
i.addLazySingleton<CameraService>(
|
||||
() => CameraService(i.get<ImagePicker>()),
|
||||
);
|
||||
|
||||
// Repository
|
||||
i.addLazySingleton<AttireRepository>(AttireRepositoryImpl.new);
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
@@ -31,16 +34,78 @@ class AttireRepositoryImpl implements AttireRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> 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';
|
||||
|
||||
await _connector.upsertStaffAttire(
|
||||
attireOptionId: itemId,
|
||||
photoUrl: mockUrl,
|
||||
Future<AttireItem> uploadPhoto(String itemId, String filePath) async {
|
||||
// 1. Upload file to Core API
|
||||
final FileUploadService uploadService = Modular.get<FileUploadService>();
|
||||
final ApiResponse uploadRes = await uploadService.uploadFile(
|
||||
filePath: filePath,
|
||||
fileName: filePath.split('/').last,
|
||||
);
|
||||
|
||||
return mockUrl;
|
||||
if (!uploadRes.code.startsWith('2')) {
|
||||
throw Exception('Upload failed: ${uploadRes.message}');
|
||||
}
|
||||
|
||||
final String fileUri = uploadRes.data?['fileUri'] as String;
|
||||
|
||||
// 2. Create signed URL for the uploaded file
|
||||
final SignedUrlService signedUrlService = Modular.get<SignedUrlService>();
|
||||
final ApiResponse signedUrlRes = await signedUrlService.createSignedUrl(
|
||||
fileUri: fileUri,
|
||||
);
|
||||
final String photoUrl = signedUrlRes.data?['signedUrl'] as String;
|
||||
|
||||
// 3. Initiate verification job
|
||||
final VerificationService verificationService =
|
||||
Modular.get<VerificationService>();
|
||||
final Staff staff = await _connector.getStaffProfile();
|
||||
|
||||
// Get item details for verification rules
|
||||
final List<AttireItem> options = await _connector.getAttireOptions();
|
||||
final AttireItem targetItem = options.firstWhere(
|
||||
(AttireItem e) => e.id == itemId,
|
||||
);
|
||||
final String dressCode =
|
||||
'${targetItem.description ?? ''} ${targetItem.label}'.trim();
|
||||
|
||||
final ApiResponse verifyRes = await verificationService.createVerification(
|
||||
type: 'attire',
|
||||
subjectType: 'worker',
|
||||
subjectId: staff.id,
|
||||
fileUri: fileUri,
|
||||
rules: <String, dynamic>{'dressCode': dressCode},
|
||||
);
|
||||
final String verificationId = verifyRes.data?['verificationId'] as String;
|
||||
|
||||
// 4. Poll for status until it's finished or timeout (max 10 seconds)
|
||||
try {
|
||||
int attempts = 0;
|
||||
bool isFinished = false;
|
||||
while (!isFinished && attempts < 5) {
|
||||
await Future<void>.delayed(const Duration(seconds: 2));
|
||||
final ApiResponse statusRes = await verificationService.getStatus(
|
||||
verificationId,
|
||||
);
|
||||
final String? status = statusRes.data?['status'] as String?;
|
||||
if (status != null && status != 'PENDING' && status != 'QUEUED') {
|
||||
isFinished = true;
|
||||
}
|
||||
attempts++;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Polling failed or timed out: $e');
|
||||
// Continue anyway, as we have the verificationId
|
||||
}
|
||||
|
||||
// 5. Update Data Connect
|
||||
await _connector.upsertStaffAttire(
|
||||
attireOptionId: itemId,
|
||||
photoUrl: photoUrl,
|
||||
verificationId: verificationId,
|
||||
);
|
||||
|
||||
// 6. Return updated AttireItem by re-fetching to get the PENDING/SUCCESS status
|
||||
final List<AttireItem> finalOptions = await _connector.getAttireOptions();
|
||||
return finalOptions.firstWhere((AttireItem e) => e.id == itemId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ abstract interface class AttireRepository {
|
||||
Future<List<AttireItem>> getAttireOptions();
|
||||
|
||||
/// Uploads a photo for a specific attire item.
|
||||
Future<String> uploadPhoto(String itemId, String filePath);
|
||||
Future<AttireItem> uploadPhoto(String itemId, String filePath);
|
||||
|
||||
/// Saves the user's attire selection and attestations.
|
||||
Future<void> saveAttire({
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
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<UploadAttirePhotoArguments, String> {
|
||||
extends UseCase<UploadAttirePhotoArguments, AttireItem> {
|
||||
/// Creates a [UploadAttirePhotoUseCase].
|
||||
UploadAttirePhotoUseCase(this._repository);
|
||||
final AttireRepository _repository;
|
||||
|
||||
@override
|
||||
Future<String> call(UploadAttirePhotoArguments arguments) {
|
||||
Future<AttireItem> call(UploadAttirePhotoArguments arguments) {
|
||||
return _repository.uploadPhoto(arguments.itemId, arguments.filePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,9 +64,21 @@ class AttireCubit extends Cubit<AttireState>
|
||||
emit(state.copyWith(selectedIds: currentSelection));
|
||||
}
|
||||
|
||||
void syncCapturedPhoto(String itemId, String url) {
|
||||
// When a photo is captured, we refresh the options to get the updated status from backend
|
||||
loadOptions();
|
||||
void syncCapturedPhoto(AttireItem item) {
|
||||
// Update the options list with the new item data
|
||||
final List<AttireItem> updatedOptions = state.options
|
||||
.map((AttireItem e) => e.id == item.id ? item : e)
|
||||
.toList();
|
||||
|
||||
// Update the photo URLs map
|
||||
final Map<String, String> updatedPhotos = Map<String, String>.from(
|
||||
state.photoUrls,
|
||||
);
|
||||
if (item.photoUrl != null) {
|
||||
updatedPhotos[item.id] = item.photoUrl!;
|
||||
}
|
||||
|
||||
emit(state.copyWith(options: updatedOptions, photoUrls: updatedPhotos));
|
||||
}
|
||||
|
||||
Future<void> save() async {
|
||||
|
||||
@@ -22,7 +22,7 @@ class AttireState extends Equatable {
|
||||
return options
|
||||
.firstWhere(
|
||||
(AttireItem e) => e.id == id,
|
||||
orElse: () => const AttireItem(id: '', label: ''),
|
||||
orElse: () => const AttireItem(id: '', code: '', label: ''),
|
||||
)
|
||||
.isMandatory;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:staff_attire/src/domain/arguments/upload_attire_photo_arguments.dart';
|
||||
import 'package:staff_attire/src/domain/usecases/upload_attire_photo_usecase.dart';
|
||||
|
||||
@@ -22,12 +23,16 @@ class AttireCaptureCubit extends Cubit<AttireCaptureState>
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
final String url = await _uploadAttirePhotoUseCase(
|
||||
final AttireItem item = await _uploadAttirePhotoUseCase(
|
||||
UploadAttirePhotoArguments(itemId: itemId, filePath: filePath),
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(status: AttireCaptureStatus.success, photoUrl: url),
|
||||
state.copyWith(
|
||||
status: AttireCaptureStatus.success,
|
||||
photoUrl: item.photoUrl,
|
||||
updatedItem: item,
|
||||
),
|
||||
);
|
||||
},
|
||||
onError: (String errorKey) => state.copyWith(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
enum AttireCaptureStatus { initial, uploading, success, failure }
|
||||
|
||||
@@ -7,24 +8,28 @@ class AttireCaptureState extends Equatable {
|
||||
this.status = AttireCaptureStatus.initial,
|
||||
this.isAttested = false,
|
||||
this.photoUrl,
|
||||
this.updatedItem,
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
final AttireCaptureStatus status;
|
||||
final bool isAttested;
|
||||
final String? photoUrl;
|
||||
final AttireItem? updatedItem;
|
||||
final String? errorMessage;
|
||||
|
||||
AttireCaptureState copyWith({
|
||||
AttireCaptureStatus? status,
|
||||
bool? isAttested,
|
||||
String? photoUrl,
|
||||
AttireItem? updatedItem,
|
||||
String? errorMessage,
|
||||
}) {
|
||||
return AttireCaptureState(
|
||||
status: status ?? this.status,
|
||||
isAttested: isAttested ?? this.isAttested,
|
||||
photoUrl: photoUrl ?? this.photoUrl,
|
||||
updatedItem: updatedItem ?? this.updatedItem,
|
||||
errorMessage: errorMessage,
|
||||
);
|
||||
}
|
||||
@@ -34,6 +39,7 @@ class AttireCaptureState extends Equatable {
|
||||
status,
|
||||
isAttested,
|
||||
photoUrl,
|
||||
updatedItem,
|
||||
errorMessage,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -298,7 +298,7 @@ class _AttireCapturePageState extends State<AttireCapturePage> {
|
||||
fullWidth: true,
|
||||
text: 'Submit Image',
|
||||
onPressed: () {
|
||||
Modular.to.pop(currentPhotoUrl);
|
||||
Modular.to.pop(state.updatedItem);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -113,10 +113,10 @@ class _AttirePageState extends State<AttirePage> {
|
||||
isUploading: false,
|
||||
uploadedPhotoUrl: state.photoUrls[item.id],
|
||||
onTap: () async {
|
||||
final String? resultUrl =
|
||||
await Navigator.push<String?>(
|
||||
final AttireItem? updatedItem =
|
||||
await Navigator.push<AttireItem?>(
|
||||
context,
|
||||
MaterialPageRoute<String?>(
|
||||
MaterialPageRoute<AttireItem?>(
|
||||
builder: (BuildContext ctx) =>
|
||||
AttireCapturePage(
|
||||
item: item,
|
||||
@@ -126,8 +126,8 @@ class _AttirePageState extends State<AttirePage> {
|
||||
),
|
||||
);
|
||||
|
||||
if (resultUrl != null && mounted) {
|
||||
cubit.syncCapturedPhoto(item.id, resultUrl);
|
||||
if (updatedItem != null && mounted) {
|
||||
cubit.syncCapturedPhoto(updatedItem);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -89,7 +89,9 @@ class AttireItemCard extends StatelessWidget {
|
||||
UiChip(
|
||||
label: statusText,
|
||||
size: UiChipSize.xSmall,
|
||||
variant: item.verificationStatus == 'SUCCESS'
|
||||
variant:
|
||||
item.verificationStatus ==
|
||||
AttireVerificationStatus.success
|
||||
? UiChipVariant.primary
|
||||
: UiChipVariant.secondary,
|
||||
),
|
||||
@@ -112,10 +114,12 @@ class AttireItemCard extends StatelessWidget {
|
||||
)
|
||||
else if (hasPhoto && !isUploading)
|
||||
Icon(
|
||||
item.verificationStatus == 'SUCCESS'
|
||||
item.verificationStatus == AttireVerificationStatus.success
|
||||
? UiIcons.check
|
||||
: UiIcons.clock,
|
||||
color: item.verificationStatus == 'SUCCESS'
|
||||
color:
|
||||
item.verificationStatus ==
|
||||
AttireVerificationStatus.success
|
||||
? UiColors.textPrimary
|
||||
: UiColors.textWarning,
|
||||
size: 24,
|
||||
|
||||
@@ -27,6 +27,7 @@ dependencies:
|
||||
path: ../../../../../design_system
|
||||
core_localization:
|
||||
path: ../../../../../core_localization
|
||||
image_picker: ^1.2.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user