diff --git a/lib/http/http.dart b/lib/http/http.dart index 053b940..e5d0701 100644 --- a/lib/http/http.dart +++ b/lib/http/http.dart @@ -95,4 +95,33 @@ class Http { logger.i(response.data.toString()); return response.data; } + + Future multipartPost( + String url, + Map fields, + String fileFieldName, + List fileBytes, + String fileName, + ) async { + final formData = FormData.fromMap({ + ...fields, + fileFieldName: MultipartFile.fromBytes( + fileBytes, + filename: fileName, + ), + }); + + final prefs = await SharedPreferences.getInstance(); + final token = prefs.getString("token") ?? ""; + + final response = await dio.post( + baseUrl + url, + data: formData, + options: Options(headers: { + 'Authorization': 'Bearer $token', + }), + ); + logger.i(response.data.toString()); + return response.data; + } } diff --git a/lib/routes.dart b/lib/routes.dart index 5b13c8a..ece0b53 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -9,6 +9,7 @@ import 'package:qpay/screens/gateway/gateway_web_screen.dart'; import 'package:qpay/screens/groups/screens/batch_create_screen.dart'; import 'package:qpay/screens/groups/screens/batch_detail_screen.dart'; import 'package:qpay/screens/groups/screens/batch_item_screen.dart'; +import 'package:qpay/screens/groups/screens/create_group_from_file_screen.dart'; import 'package:qpay/screens/groups/screens/group_create_screen.dart'; import 'package:qpay/screens/groups/screens/group_detail_screen.dart'; import 'package:qpay/screens/groups/screens/groups_screen.dart'; @@ -226,6 +227,11 @@ GoRouter buildRouter() { path: '/groups/create', builder: (context, state) => const GroupCreateScreen(), ), + GoRoute( + path: '/groups/create-from-file', + builder: (context, state) => + const CreateGroupFromFileScreen(), + ), GoRoute( path: '/groups/:groupId', builder: (context, state) { diff --git a/lib/screens/groups/controllers/group_controller.dart b/lib/screens/groups/controllers/group_controller.dart index 7c85ab3..89b4499 100644 --- a/lib/screens/groups/controllers/group_controller.dart +++ b/lib/screens/groups/controllers/group_controller.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:qpay/http/http.dart'; import 'package:qpay/models/api_response.dart'; import 'package:qpay/models/pageable_model.dart'; +import 'package:qpay/screens/groups/models/create_group_from_file_response.dart'; import 'package:qpay/screens/groups/models/recipient_group_model.dart'; class GroupScreenModel { @@ -80,8 +81,9 @@ class GroupController extends ChangeNotifier { '/public/recipient-groups?${buildQueryParameters(params)}', ); final response = _unwrap(raw); - final pageableModel = - PageableModel.fromJson(response as Map); + final pageableModel = PageableModel.fromJson( + response as Map, + ); final newGroups = pageableModel.content .map((e) => RecipientGroup.fromJson(e as Map)) @@ -113,10 +115,7 @@ class GroupController extends ChangeNotifier { } } - Future searchGroups({ - required String userId, - String? name, - }) async { + Future searchGroups({required String userId, String? name}) async { model.searchQuery = name ?? ''; await listGroups(userId: userId, page: 0, name: name); } @@ -252,4 +251,50 @@ class GroupController extends ChangeNotifier { await deleteGroup(groupId: id, userId: userId); } } -} \ No newline at end of file + + Future> createGroupFromFile({ + required String groupName, + required String userId, + String? description, + required String currency, + required List fileBytes, + required String fileName, + }) async { + model.isLoading = true; + if (!_disposed) notifyListeners(); + + try { + final fields = { + 'groupName': groupName, + 'userId': userId, + 'currency': currency, + }; + if (description != null && description.isNotEmpty) { + fields['description'] = description; + } + + final raw = await http.multipartPost( + '/public/recipient-groups/create-from-file', + fields, + 'file', + fileBytes, + fileName, + ); + final response = _unwrap(raw); + final result = CreateGroupFromFileResponse.fromJson( + response as Map, + ); + + model.isLoading = false; + if (!_disposed) notifyListeners(); + + return ApiResponse.success(result); + } catch (e) { + logger.e(e); + model.errorMessage = e.toString(); + model.isLoading = false; + if (!_disposed) notifyListeners(); + return ApiResponse.failure('Failed to create group from file'); + } + } +} diff --git a/lib/screens/groups/models/create_group_from_file_response.dart b/lib/screens/groups/models/create_group_from_file_response.dart new file mode 100644 index 0000000..d533ff4 --- /dev/null +++ b/lib/screens/groups/models/create_group_from_file_response.dart @@ -0,0 +1,48 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:qpay/screens/groups/models/group_batch_model.dart'; + +part 'create_group_from_file_response.g.dart'; + +@JsonSerializable(explicitToJson: true) +class FileUploadError { + final int lineNumber; + final String account; + final String name; + final String message; + + const FileUploadError({ + required this.lineNumber, + required this.account, + required this.name, + required this.message, + }); + + factory FileUploadError.fromJson(Map json) => + _$FileUploadErrorFromJson(json); + + Map toJson() => _$FileUploadErrorToJson(this); +} + +@JsonSerializable(explicitToJson: true) +class CreateGroupFromFileResponse { + final GroupBatch? batch; + final int totalRows; + final int successCount; + final int errorCount; + final List errors; + + const CreateGroupFromFileResponse({ + this.batch, + required this.totalRows, + required this.successCount, + required this.errorCount, + required this.errors, + }); + + factory CreateGroupFromFileResponse.fromJson(Map json) => + _$CreateGroupFromFileResponseFromJson(json); + + Map toJson() => _$CreateGroupFromFileResponseToJson(this); + + bool get isSuccess => errors.isEmpty; +} diff --git a/lib/screens/groups/models/create_group_from_file_response.g.dart b/lib/screens/groups/models/create_group_from_file_response.g.dart new file mode 100644 index 0000000..46bc40f --- /dev/null +++ b/lib/screens/groups/models/create_group_from_file_response.g.dart @@ -0,0 +1,47 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'create_group_from_file_response.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +FileUploadError _$FileUploadErrorFromJson(Map json) => + FileUploadError( + lineNumber: (json['lineNumber'] as num).toInt(), + account: json['account'] as String, + name: json['name'] as String, + message: json['message'] as String, + ); + +Map _$FileUploadErrorToJson(FileUploadError instance) => + { + 'lineNumber': instance.lineNumber, + 'account': instance.account, + 'name': instance.name, + 'message': instance.message, + }; + +CreateGroupFromFileResponse _$CreateGroupFromFileResponseFromJson( + Map json, +) => CreateGroupFromFileResponse( + batch: json['batch'] == null + ? null + : GroupBatch.fromJson(json['batch'] as Map), + totalRows: (json['totalRows'] as num).toInt(), + successCount: (json['successCount'] as num).toInt(), + errorCount: (json['errorCount'] as num).toInt(), + errors: (json['errors'] as List) + .map((e) => FileUploadError.fromJson(e as Map)) + .toList(), +); + +Map _$CreateGroupFromFileResponseToJson( + CreateGroupFromFileResponse instance, +) => { + 'batch': instance.batch?.toJson(), + 'totalRows': instance.totalRows, + 'successCount': instance.successCount, + 'errorCount': instance.errorCount, + 'errors': instance.errors.map((e) => e.toJson()).toList(), +}; diff --git a/lib/screens/groups/screens/batch_detail_screen.dart b/lib/screens/groups/screens/batch_detail_screen.dart index ccc7e22..a7ee691 100644 --- a/lib/screens/groups/screens/batch_detail_screen.dart +++ b/lib/screens/groups/screens/batch_detail_screen.dart @@ -1737,28 +1737,17 @@ class _BatchDetailScreenState extends State children: [ StatusChip(status: status), const SizedBox(width: 8), - Expanded( - child: Row( - children: [ - Text( - '$currency ', - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: isDark ? Colors.white60 : Colors.grey.shade600, - ), + if (parsedDate != null) + Expanded( + child: Text( + DateFormat.yMMMd().add_jm().format(parsedDate), + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w400, + color: isDark ? Colors.white38 : Colors.grey.shade500, ), - Text( - value?.toStringAsFixed(2) ?? '—', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w700, - color: isDark ? Colors.white : Colors.black87, - ), - ), - ], + ), ), - ), _buildRefreshButton(theme, isDark), ], ), @@ -1766,15 +1755,26 @@ class _BatchDetailScreenState extends State // Row 2: Date + Summary chips Row( children: [ - if (parsedDate != null) - Text( - DateFormat.yMMMd().add_jm().format(parsedDate), - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w400, - color: isDark ? Colors.white38 : Colors.grey.shade500, + Row( + children: [ + Text( + '$currency ', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: isDark ? Colors.white60 : Colors.grey.shade600, + ), ), - ), + Text( + value?.toStringAsFixed(2) ?? '—', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: isDark ? Colors.white : Colors.black87, + ), + ), + ], + ), if (batch.count != null && parsedDate != null) const SizedBox(width: 6), if (batch.count != null) diff --git a/lib/screens/groups/screens/create_group_from_file_screen.dart b/lib/screens/groups/screens/create_group_from_file_screen.dart new file mode 100644 index 0000000..d4ebe60 --- /dev/null +++ b/lib/screens/groups/screens/create_group_from_file_screen.dart @@ -0,0 +1,428 @@ +import 'package:flutter/material.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:go_router/go_router.dart'; +import 'package:qpay/screens/groups/controllers/group_controller.dart'; +import 'package:qpay/screens/groups/models/create_group_from_file_response.dart'; +import 'package:qpay/models/responsive_policy.dart'; +import 'package:qpay/widgets/app_snack_bar.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class CreateGroupFromFileScreen extends StatefulWidget { + const CreateGroupFromFileScreen({super.key}); + + @override + State createState() => + _CreateGroupFromFileScreenState(); +} + +class _CreateGroupFromFileScreenState extends State { + late GroupController controller; + final _formKey = GlobalKey(); + final _groupNameController = TextEditingController(); + final _descriptionController = TextEditingController(); + String _currency = 'USD'; + List? _fileBytes; + String? _selectedFileName; + + @override + void initState() { + super.initState(); + controller = GroupController(context); + } + + @override + void dispose() { + _groupNameController.dispose(); + _descriptionController.dispose(); + controller.dispose(); + super.dispose(); + } + + Future _pickFile() async { + final result = await FilePicker.pickFiles( + type: FileType.custom, + allowedExtensions: ['csv'], + withData: true, + ); + + if (result != null && result.files.isNotEmpty) { + final file = result.files.first; + if (file.bytes != null) { + setState(() { + _fileBytes = file.bytes; + _selectedFileName = file.name; + }); + } + } + } + + Future _submit() async { + if (!_formKey.currentState!.validate()) return; + + if (_fileBytes == null || _selectedFileName == null) { + AppSnackBar.show(context, 'Please select a CSV file to upload.'); + return; + } + + final prefs = await SharedPreferences.getInstance(); + final userId = prefs.getString('userId') ?? ''; + + final result = await controller.createGroupFromFile( + groupName: _groupNameController.text.trim(), + userId: userId, + description: _descriptionController.text.trim().isEmpty + ? null + : _descriptionController.text.trim(), + currency: _currency, + fileBytes: _fileBytes!, + fileName: _selectedFileName!, + ); + + if (!mounted) return; + + if (result.isSuccess) { + final response = result.data!; + if (response.isSuccess && response.batch != null) { + final batch = response.batch!; + final groupId = batch.recipientGroupId ?? ''; + + AppSnackBar.showSuccess( + context, + 'Group & batch created successfully. ' + '${response.successCount} of ${response.totalRows} rows processed.', + ); + + if (groupId.isNotEmpty && batch.id != null) { + context.pushReplacement( + '/groups/$groupId/batches/${batch.id}', + ); + } + } else { + // Partial failure — show errors + _showErrorsBottomSheet(response); + } + } else { + AppSnackBar.showError( + context, + result.error ?? 'Failed to create group from file.', + ); + } + } + + void _showErrorsBottomSheet(CreateGroupFromFileResponse response) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (sheetContext) { + return Container( + decoration: BoxDecoration( + color: isDark ? const Color(0xFF1A1A2E) : Colors.white, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(24), + ), + ), + padding: const EdgeInsets.fromLTRB(20, 12, 20, 24), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 40, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + Row( + children: [ + Icon(Icons.warning_amber_rounded, + color: Colors.orange.shade700, size: 24), + const SizedBox(width: 10), + Text( + 'Upload completed with errors', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: isDark ? Colors.white : Colors.black87, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + '${response.errorCount} of ${response.totalRows} rows failed. ' + '${response.successCount} rows succeeded.', + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 16), + if (response.errors.isNotEmpty) ...[ + Flexible( + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: + MediaQuery.of(sheetContext).size.height * 0.4, + ), + child: ListView.separated( + shrinkWrap: true, + itemCount: response.errors.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (_, index) { + final error = response.errors[index]; + return ListTile( + dense: true, + contentPadding: EdgeInsets.zero, + leading: CircleAvatar( + radius: 14, + backgroundColor: Colors.red.shade50, + child: Text( + '${error.lineNumber}', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: Colors.red.shade700, + ), + ), + ), + title: Text( + error.name.isNotEmpty + ? '${error.name} (${error.account})' + : error.account, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + subtitle: Text( + error.message, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ); + }, + ), + ), + ), + const SizedBox(height: 16), + ], + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Navigator.of(sheetContext).pop(), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + ), + child: const Text('Dismiss'), + ), + ), + ], + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Create from File'), + centerTitle: true, + ), + body: ListenableBuilder( + listenable: controller, + builder: (context, _) { + return Center( + child: LayoutBuilder( + builder: (context, constraints) { + final width = constraints.maxWidth > ResponsivePolicy.md + ? ResponsivePolicy.md.toDouble() + : constraints.maxWidth; + return SizedBox( + width: width, + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextFormField( + controller: _groupNameController, + textInputAction: TextInputAction.next, + decoration: InputDecoration( + labelText: 'Group Name', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + validator: (v) => + (v == null || v.trim().isEmpty) + ? 'Group name is required' + : null, + ), + const SizedBox(height: 16), + TextFormField( + controller: _descriptionController, + textInputAction: TextInputAction.next, + decoration: InputDecoration( + labelText: 'Description (optional)', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + const SizedBox(height: 16), + DropdownButtonFormField( + initialValue: _currency, + decoration: InputDecoration( + labelText: 'Currency', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + items: const [ + DropdownMenuItem( + value: 'USD', + child: Text('USD'), + ), + DropdownMenuItem( + value: 'ZWG', + child: Text('ZWG'), + ), + ], + onChanged: (v) { + if (v != null) setState(() => _currency = v); + }, + ), + const SizedBox(height: 24), + Text( + 'Recipients File', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.grey.shade800, + ), + ), + const SizedBox(height: 4), + Text( + 'Upload a CSV file with columns: name, account, amount, billProductName', + style: TextStyle( + fontSize: 13, + color: Colors.grey.shade600, + ), + ), + const SizedBox(height: 12), + InkWell( + onTap: _pickFile, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + border: Border.all( + color: _fileBytes != null + ? Theme.of(context).colorScheme.primary + : Colors.grey.shade300, + width: 1.5, + style: BorderStyle.solid, + ), + borderRadius: BorderRadius.circular(12), + color: _fileBytes != null + ? Theme.of(context) + .colorScheme + .primary + .withAlpha(12) + : Colors.grey.shade50, + ), + child: Column( + children: [ + Icon( + _fileBytes != null + ? Icons.insert_drive_file + : Icons.cloud_upload_outlined, + size: 40, + color: _fileBytes != null + ? Theme.of(context).colorScheme.primary + : Colors.grey.shade400, + ), + const SizedBox(height: 8), + Text( + _selectedFileName ?? 'Tap to select CSV file', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: _fileBytes != null + ? Theme.of(context) + .colorScheme + .primary + : Colors.grey.shade600, + ), + textAlign: TextAlign.center, + ), + if (_fileBytes == null) ...[ + const SizedBox(height: 4), + Text( + 'Accepted format: .csv', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade500, + ), + ), + ], + ], + ), + ), + ), + const SizedBox(height: 32), + ElevatedButton( + onPressed: controller.model.isLoading + ? null + : _submit, + style: ElevatedButton.styleFrom( + padding: + const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: controller.model.isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Text( + 'Upload & Create', + style: TextStyle(fontSize: 16), + ), + ), + ], + ), + ), + ), + ); + }, + ), + ); + }, + ), + ); + } +} \ No newline at end of file diff --git a/lib/screens/groups/screens/groups_screen.dart b/lib/screens/groups/screens/groups_screen.dart index 3f599c3..b55fc86 100644 --- a/lib/screens/groups/screens/groups_screen.dart +++ b/lib/screens/groups/screens/groups_screen.dart @@ -196,11 +196,149 @@ class _GroupsScreenState extends State { ); } - _createGroup() async { - final response = await context.push('/groups/create'); - if (response == true) { - _loadData(); - } + _createGroup() { + _showCreateGroupBottomSheet(); + } + + void _showCreateGroupBottomSheet() { + final isDark = Theme.of(context).brightness == Brightness.dark; + + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + builder: (sheetContext) { + return Container( + decoration: BoxDecoration( + color: isDark ? const Color(0xFF1A1A2E) : Colors.white, + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + ), + padding: const EdgeInsets.fromLTRB(20, 12, 20, 24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Center( + child: Container( + width: 40, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + Text( + 'Create Group', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: isDark ? Colors.white : Colors.black87, + ), + ), + const SizedBox(height: 6), + Text( + 'Choose how you would like to create a group.', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Colors.grey.shade500, + ), + ), + const SizedBox(height: 20), + _buildCreateOption( + sheetContext, + icon: Icons.edit_note, + title: 'Create via Form', + subtitle: 'Manually enter group details and recipients', + onTap: () async { + Navigator.of(sheetContext).pop(); + final response = await context.push('/groups/create'); + if (response == true) { + _loadData(); + } + }, + isDark: isDark, + ), + const SizedBox(height: 12), + _buildCreateOption( + sheetContext, + icon: Icons.upload_file, + title: 'Create from File', + subtitle: 'Upload a CSV file with group & batch data', + onTap: () { + Navigator.of(sheetContext).pop(); + context.push('/groups/create-from-file'); + }, + isDark: isDark, + ), + ], + ), + ); + }, + ); + } + + Widget _buildCreateOption( + BuildContext sheetContext, { + required IconData icon, + required String title, + required String subtitle, + required VoidCallback onTap, + required bool isDark, + }) { + final theme = Theme.of(context); + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isDark + ? Colors.white.withValues(alpha: 0.05) + : Colors.grey.shade50, + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: isDark + ? Colors.white.withValues(alpha: 0.08) + : Colors.grey.shade200, + ), + ), + child: Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: theme.colorScheme.primary.withAlpha(25), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: theme.colorScheme.primary, size: 24), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: isDark ? Colors.white : Colors.black87, + ), + ), + const SizedBox(height: 2), + Text( + subtitle, + style: TextStyle(fontSize: 13, color: Colors.grey.shade600), + ), + ], + ), + ), + Icon(Icons.chevron_right, color: Colors.grey.shade400), + ], + ), + ), + ); } Widget _buildIconButton({ diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 086c28b..5bb8f4e 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import file_picker import firebase_core import path_provider_foundation import share_plus @@ -13,6 +14,7 @@ import url_launcher_macos import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 4892f79..21f8464 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -185,6 +185,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + dbus: + dependency: transitive + description: + name: dbus + sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" + url: "https://pub.dev" + source: hosted + version: "0.7.14" decimal: dependency: "direct main" description: @@ -233,6 +241,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: f13a03000d942e476bc1ff0a736d2e9de711d2f89a95cd4c1d88f861c3348387 + url: "https://pub.dev" + source: hosted + version: "11.0.2" firebase_core: dependency: "direct main" description: @@ -294,6 +310,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785" + url: "https://pub.dev" + source: hosted + version: "2.0.35" flutter_svg: dependency: "direct main" description: @@ -1111,4 +1135,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.10.0 <4.0.0" - flutter: ">=3.35.0" + flutter: ">=3.38.0" diff --git a/pubspec.yaml b/pubspec.yaml index 478f532..0b1e735 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,7 @@ dependencies: sdk: flutter decimal: ^3.2.4 icons_launcher: ^3.1.0 + file_picker: ^11.0.2 dev_dependencies: flutter_test: