chore: update dependencies and version in pubspec.yaml

- Bump version to 1.10.0+14
- Update http from ^1.1.0 to ^1.6.0
- Update google_sign_in from ^7.1.1 to ^7.2.0
- Update flutter_secure_storage from ^10.3.1 to ^11.2.0
- Update firebase_core from ^4.12.1 to ^4.15.0
- Update firebase_crashlytics from ^5.2.6 to ^5.4.0
- Update go_router from ^17.2.3 to ^18.0.1
- Update flutter_riverpod from ^3.3.1 to ^3.4.3
- Update table_calendar from ^3.2.0 to ^3.2.1
- Update intl from ^0.20.2 to ^0.20.3
- Update url_launcher from ^6.3.1 to ^6.3.2
- Update file_picker from ^11.0.3 to ^13.1.0
- Update google_fonts from ^8.1.0 to ^8.2.1
- Update lucide_icons_flutter from ^3.1.15 to ^3.1.20
- Add cross_file dependency for testing

test: add FakePlatformFile for testing file uploads

- Introduce FakePlatformFile to mock file picker behavior in tests
- Update attachment_upload_test, csv_import_provider_test, support_ticket_test, and csv_import_sheet_test to use FakePlatformFile
This commit is contained in:
Ashwin 2026-09-27 18:58:13 +05:30
parent 2c70c62eaf
commit d920cc294f
13 changed files with 321 additions and 356 deletions

View file

@ -433,14 +433,16 @@ class CsvImportNotifier extends Notifier<CsvImportState> {
/// file is refused without loading it, and the size again on the bytes.
Future<void> choose(PlatformFile? picked) async {
if (picked == null) return;
final early = _refuse(picked.name, picked.size);
// `lengthSync` is what the picker reported, without I/O. Unknown counts
// as 0 here; the bytes are checked again below either way.
final early = _refuse(picked.name, picked.lengthSync() ?? 0);
if (early != null) {
state = CsvImportState(error: early);
return;
}
final Uint8List bytes;
try {
bytes = picked.bytes ?? await picked.xFile.readAsBytes();
bytes = await picked.readAsBytes();
} catch (_) {
if (ref.mounted) {
state = const CsvImportState(

View file

@ -75,7 +75,7 @@ class SupportRepository {
required String body,
PlatformFile? attachment,
}) async {
final invalid = _attachmentError(attachment);
final invalid = await _attachmentError(attachment);
if (invalid != null) return SupportMutationResult(error: invalid);
final response = attachment == null
? await _api.post(ApiConfig.supportTickets, {
@ -102,7 +102,7 @@ class SupportRepository {
required String body,
PlatformFile? attachment,
}) async {
final invalid = _attachmentError(attachment);
final invalid = await _attachmentError(attachment);
if (invalid != null) return SupportMutationResult(error: invalid);
final response = attachment == null
? await _api.post(ApiConfig.supportTicketReplies(ticketId), {
@ -118,12 +118,13 @@ class SupportRepository {
return _ticketResult(response);
}
String? _attachmentError(PlatformFile? file) {
Future<String?> _attachmentError(PlatformFile? file) async {
if (file == null) return null;
if ((file.path ?? '').isEmpty) {
final size = await file.length();
if ((file.path ?? '').isEmpty || size == null) {
return 'That file could not be read. Try picking it again.';
}
if (file.size > attachmentMaxBytes) {
if (size > attachmentMaxBytes) {
return 'Files must be 25 MB or smaller.';
}
return null;

View file

@ -120,14 +120,7 @@ class _InvoiceTemplateFormScreenState
// images because `logo` is an ImageField: Pillow refuses anything else
// server-side, and a 400 after an upload is a poor way to learn that.
final result = await selectAttachment(
pickFile: () async {
final picked = await FilePicker.pickFiles(
type: FileType.image,
withReadStream: false,
);
if (picked == null || picked.files.isEmpty) return null;
return picked.files.first;
},
pickFile: () => FilePicker.pickFile(type: FileType.image),
);
if (result.cancelled || !mounted) return;
if (result.error != null) {

View file

@ -74,10 +74,16 @@ Future<AttachmentSelectionResult> selectAttachment({
'That file could not be read. Try picking it again.',
);
}
if (picked.size > attachmentMaxBytes) {
final size = await picked.length();
if (size == null) {
return const AttachmentSelectionResult.failed(
'That file could not be read. Try picking it again.',
);
}
if (size > attachmentMaxBytes) {
final limit = attachmentMaxBytes ~/ (1024 * 1024);
return AttachmentSelectionResult.failed(
'${picked.name} is ${_megabytes(picked.size)} MB. '
'${picked.name} is ${_megabytes(size)} MB. '
'Files must be $limit MB or smaller.',
);
}
@ -117,12 +123,12 @@ Future<AttachmentDownloadResult> downloadAttachment({
}
}
Future<String?> _saveFile(Uint8List bytes, String fileName) =>
FilePicker.saveFile(
Future<String?> _saveFile(Uint8List bytes, String fileName) async =>
(await FilePicker.saveFile(
dialogTitle: 'Save attachment',
fileName: fileName,
bytes: bytes,
);
))?.toString();
/// Remove the temporary copy made by the native picker after a form uploads it.
Future<void> clearAttachmentPickerCache() => _clearPickerCache();
@ -175,13 +181,21 @@ Future<AttachmentUploadResult> pickAndUploadAttachment({
);
}
if (picked.size > attachmentMaxBytes) {
final size = await picked.length();
if (size == null) {
if (pickFile == null) await _clearPickerCache();
return const AttachmentUploadResult.failed(
'That file could not be read. Try picking it again.',
);
}
if (size > attachmentMaxBytes) {
// Clear it here too. This is the case where the copy matters most: the
// file that was too big to send is the one it hurts to keep.
if (pickFile == null) await _clearPickerCache();
final limit = attachmentMaxBytes ~/ (1024 * 1024);
return AttachmentUploadResult.failed(
'${picked.name} is ${_megabytes(picked.size)} MB. '
'${picked.name} is ${_megabytes(size)} MB. '
'Files must be $limit MB or smaller.',
);
}
@ -203,15 +217,11 @@ Future<AttachmentUploadResult> pickAndUploadAttachment({
return AttachmentUploadResult.uploaded(response.data);
}
Future<PlatformFile?> _pickOneFile() async {
final result = await FilePicker.pickFiles(withReadStream: false);
if (result == null || result.files.isEmpty) return null;
return result.files.first;
}
Future<PlatformFile?> _pickOneFile() => FilePicker.pickFile();
/// Drop the picker's cached copy of what was just uploaded.
///
/// `pickFiles` copies the chosen file into the app's cache directory, so
/// `pickFile` copies the chosen file into the app's cache directory, so
/// without this every attachment a person sends stays on their phone a second
/// time, in a directory they never see. Only when the real picker ran: an
/// injected one has nothing to clear, and the platform channel is not there in

View file

@ -81,9 +81,9 @@ Future<RecordExportResult> exportRecordsCsv({
}
}
Future<String?> _saveWithPicker(Uint8List bytes, String fileName) =>
FilePicker.saveFile(
Future<String?> _saveWithPicker(Uint8List bytes, String fileName) async =>
(await FilePicker.saveFile(
dialogTitle: 'Save CSV export',
fileName: fileName,
bytes: bytes,
);
))?.toString();

View file

@ -70,13 +70,9 @@ class _CsvImportSheetState extends ConsumerState<CsvImportSheet> {
if (injected == null && picked != null) await clearAttachmentPickerCache();
}
static Future<PlatformFile?> _pickOne() async {
// Any type, then refused by name: a CSV is labelled text/plain by some
// Android file managers, and filtering on MIME type would hide it.
final result = await FilePicker.pickFiles();
if (result == null || result.files.isEmpty) return null;
return result.files.first;
}
// Any type, then refused by name: a CSV is labelled text/plain by some
// Android file managers, and filtering on MIME type would hide it.
static Future<PlatformFile?> _pickOne() => FilePicker.pickFile();
/// Ask where to keep [content] as [fileName]. `null` when the person
/// closed the dialog, a sentence to show otherwise.
@ -118,12 +114,14 @@ class _CsvImportSheetState extends ConsumerState<CsvImportSheet> {
if (mounted) setState(() => _errorsNote = note);
}
static Future<String?> _saveWithPicker(String fileName, List<int> bytes) =>
FilePicker.saveFile(
dialogTitle: 'Save CSV file',
fileName: fileName,
bytes: Uint8List.fromList(bytes),
);
static Future<String?> _saveWithPicker(
String fileName,
List<int> bytes,
) async => (await FilePicker.saveFile(
dialogTitle: 'Save CSV file',
fileName: fileName,
bytes: Uint8List.fromList(bytes),
))?.toString();
Future<void> _commit() async {
final ok = await _notifier.commit();

View file

@ -1,38 +1,30 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: a49d6cf99e8d8e7a8e93668d09ced0bbdb954d0b4fccc2f5f9241c6b87fad95c
url: "https://pub.dev"
source: hosted
version: "99.0.0"
_flutterfire_internals:
dependency: transitive
description:
name: _flutterfire_internals
sha256: "6727cf2ced9b104abca9daa278380be2eca2b98ce33d4b46f11708e387dc6b4d"
sha256: f6966125633a34f82d9be2ee895b755cff3554087b1d00a9eb884e5947f4bca9
url: "https://pub.dev"
source: hosted
version: "1.3.76"
analyzer:
version: "1.3.77"
android_file_picker:
dependency: transitive
description:
name: analyzer
sha256: "663efa951fb8a45e06f491223a604c93820598f20e6a99c25617a1576065e8b7"
name: android_file_picker
sha256: "14ab27769b54c48d5a8a71aa9858372b7a3ae77572ea0a8aee744643c5d8c53d"
url: "https://pub.dev"
source: hosted
version: "12.1.0"
version: "2.0.0"
archive:
dependency: transitive
description:
name: archive
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
sha256: "6c5bcd986e06b94e3c40244af471750840a3d2341d1f9763a1100a14add517b4"
url: "https://pub.dev"
source: hosted
version: "4.0.9"
version: "4.3.0"
args:
dependency: transitive
description:
@ -73,14 +65,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.4"
cli_config:
dependency: transitive
description:
name: cli_config
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
url: "https://pub.dev"
source: hosted
version: "0.2.0"
cli_util:
dependency: transitive
description:
@ -93,18 +77,18 @@ packages:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
sha256: e51d50bca3217c9a9fa2b41a30e4a38971133f5f9ec7a3d57bae095007f1d28e
url: "https://pub.dev"
source: hosted
version: "1.1.2"
version: "1.1.3"
code_assets:
dependency: transitive
description:
name: code_assets
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
sha256: "828110d598123b5ea96c00c9f3c72105bf79f8ee36c20a39b26209ade421ec57"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "2.1.0"
collection:
dependency: transitive
description:
@ -113,30 +97,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d"
url: "https://pub.dev"
source: hosted
version: "1.15.1"
cross_file:
dependency: transitive
dependency: "direct dev"
description:
name: cross_file
sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51"
sha256: f141ea4f277af142a0356955707f6556f37b03947d39d55585981a06ca437bd6
url: "https://pub.dev"
source: hosted
version: "0.3.5+4"
version: "0.3.5+5"
crypto:
dependency: transitive
description:
@ -153,14 +121,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.9"
cupertino_ui:
dependency: transitive
description:
name: cupertino_ui
sha256: c23816577a1280cff409e989541767887011f8f1d8c1863a794ed308af46f2a9
url: "https://pub.dev"
source: hosted
version: "1.1.1"
dbus:
dependency: transitive
description:
name: dbus
sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645"
sha256: f7db8504e715b835f5ebe5fe74604ccd961d504c73389a5d8e81bafc2e2f3612
url: "https://pub.dev"
source: hosted
version: "0.7.14"
version: "0.8.0"
equatable:
dependency: transitive
description:
@ -185,6 +161,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.0"
ffi_leak_tracker:
dependency: transitive
description:
name: ffi_leak_tracker
sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
url: "https://pub.dev"
source: hosted
version: "0.1.2"
file:
dependency: transitive
description:
@ -197,50 +181,82 @@ packages:
dependency: "direct main"
description:
name: file_picker
sha256: "29cc1fdb20613876cc7afc529738c1c0f11a9ca159b010edad0c566ac330847e"
sha256: "98c0b156b6380ba55bc767a14e2e6b3feb246e713ad40092424596fa79005bea"
url: "https://pub.dev"
source: hosted
version: "11.0.3"
version: "13.1.0"
file_picker_darwin:
dependency: transitive
description:
name: file_picker_darwin
sha256: "51aef9f4c80449c736e7cc02198675fda73689f0bd45a84da642cfeab0250d46"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
file_picker_linux:
dependency: transitive
description:
name: file_picker_linux
sha256: "9c870771500c758eb46506c98532c3ee60e6e51d0a62dcde24a849ef213ea5ce"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
file_picker_platform_interface:
dependency: transitive
description:
name: file_picker_platform_interface
sha256: bbdc085a6f168e63f147e9ce8988f79fa0800a58e7f25a48f762c60837107ba5
url: "https://pub.dev"
source: hosted
version: "4.0.0"
file_picker_web:
dependency: transitive
description:
name: file_picker_web
sha256: b3004268da0c1b52baa18df430e7ec4438194de855a807d26b447cbf07ef3496
url: "https://pub.dev"
source: hosted
version: "4.0.0"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: "9478ca6700c02d315c6aba37e206e612317f98bb4f335bb1cbd6e0ce67dcf764"
sha256: "5fbbaa2aeb08a52feee559f92a62f5a6b04c9bdff81c8f608636771cf0d7b680"
url: "https://pub.dev"
source: hosted
version: "4.13.0"
version: "4.15.0"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: e28f9afdcb5b0f0a8ea74ea3b322f5a7592c81cb45dd9d189913bdae08a2089a
sha256: "9bfbc85faca09346471ac56fbcee00757ebcfd85887c6f602764eff0001902d8"
url: "https://pub.dev"
source: hosted
version: "8.1.0"
version: "8.1.1"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: f471a288b0101a45567548322ac4a5ad31e3ecbf87a576dcc0e424e6a11e04ea
sha256: "8ed84f5001dbd7a0390e2818d46bc858ceebb970edededdfc8d6552c76fc2f6e"
url: "https://pub.dev"
source: hosted
version: "3.10.0"
version: "3.12.0"
firebase_crashlytics:
dependency: "direct main"
description:
name: firebase_crashlytics
sha256: "96c1d85de9eddc07061d3f7e2fb75596e75a45c9cec9c2e3a7cc9f97e6f3a748"
sha256: f37fe0df162f08bd549114b784037c73a093c64d22625632f97dbbddf18669b9
url: "https://pub.dev"
source: hosted
version: "5.2.7"
version: "5.4.0"
firebase_crashlytics_platform_interface:
dependency: transitive
description:
name: firebase_crashlytics_platform_interface
sha256: "47d83b71fd39c580297c696a507a7c2652680ea2045e40c6c4e3c371b0ee7bc6"
sha256: "8270dcdb1800db3e5f888ce30a2d5e8f92c379c461f022297c83b2c794aa1792"
url: "https://pub.dev"
source: hosted
version: "3.8.27"
version: "3.9.0"
fixnum:
dependency: transitive
description:
@ -278,54 +294,51 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_plugin_android_lifecycle:
flutter_localizations:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
url: "https://pub.dev"
source: hosted
version: "2.0.35"
description: flutter
source: sdk
version: "0.0.0"
flutter_riverpod:
dependency: "direct main"
description:
name: flutter_riverpod
sha256: "56e81e662e6e54e59b231da57e90ac0d06ac67d8e3f2273c129a37ae6282b40c"
sha256: "2b7f9d2a3c730ac1a98221b420cef966b990fca7ab546ede324642ca57a533e3"
url: "https://pub.dev"
source: hosted
version: "3.4.2"
version: "3.4.3"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e"
sha256: d4e1fb6b2cb524868929e78dc0282fa000554b22060fb53789dc481c9fc95bb8
url: "https://pub.dev"
source: hosted
version: "10.3.1"
version: "11.2.0"
flutter_secure_storage_darwin:
dependency: transitive
description:
name: flutter_secure_storage_darwin
sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149"
sha256: a031ceac9b070e62ef183cd7f7d1a8798ebde402d5049f0fe6eb3a51bd797a21
url: "https://pub.dev"
source: hosted
version: "0.3.2"
version: "0.4.3"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: "76fa9c841b3b1619fc5b5bc36efc7d158fa2356f223b6caeb1d0c80a54168546"
sha256: caa75bd78f017422912e3a904933113b2428eb79f981ac37acd2dd2a700458ea
url: "https://pub.dev"
source: hosted
version: "3.0.2"
version: "3.0.3"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: "788060052712555182aba55ecb5f8b6e5cb9cfe8f776c83249a61fe3ce877db4"
sha256: "64951127f001f546891c86f414972bf49ef2c4c8a4c92c65f5790cc0c9100045"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
version: "2.1.1"
flutter_secure_storage_web:
dependency: transitive
description:
@ -338,10 +351,10 @@ packages:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613"
sha256: "471951813a97006d899db4948acc654a4f28c440083ea08178935ce20b173ec1"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
version: "4.2.2"
flutter_test:
dependency: "direct dev"
description: flutter
@ -352,30 +365,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
go_router:
dependency: "direct main"
description:
name: go_router
sha256: "55973bf1b27790489f8710ea615fe939f911ddcc5986fe97305e4fc19fa29ee9"
sha256: "008ab21d89d0de8ccd79586838f7533f126c2d894f50bf25f2947888d3c225b5"
url: "https://pub.dev"
source: hosted
version: "17.4.0"
version: "18.0.1"
google_fonts:
dependency: "direct main"
description:
@ -404,18 +401,18 @@ packages:
dependency: transitive
description:
name: google_sign_in_android
sha256: "57782125965ca87b03d42a147d40b046f1fd6a09141a58913e1b86671a5ef22e"
sha256: c403315d87aba1f815a0a401093f97808cab6cb2bdd11ca431ff8587fd7f1c00
url: "https://pub.dev"
source: hosted
version: "7.2.16"
version: "7.2.17"
google_sign_in_ios:
dependency: transitive
description:
name: google_sign_in_ios
sha256: ac1e4c1205267cb7999d1d81333fccffdfda29e853f434bbaf71525498bb6950
sha256: c2154d2849310a0286aab2e35e3bee585f2034afdf8ac364c4d7b3638e4c40b6
url: "https://pub.dev"
source: hosted
version: "6.3.0"
version: "6.3.5"
google_sign_in_platform_interface:
dependency: transitive
description:
@ -436,10 +433,10 @@ packages:
dependency: transitive
description:
name: hooks
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
sha256: eaac480a35ec0814146c2c48d96aaa829e0e44a7662c88ae84c9edf4bc35651f
url: "https://pub.dev"
source: hosted
version: "2.0.2"
version: "2.2.0"
http:
dependency: "direct main"
description:
@ -448,14 +445,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
@ -468,10 +457,10 @@ packages:
dependency: transitive
description:
name: image
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
sha256: a1e7f4951e538a568e14b856702afc9ae1d2f4b202daced8d22c1b9cd211ce89
url: "https://pub.dev"
source: hosted
version: "4.8.0"
version: "4.10.1"
intl:
dependency: "direct main"
description:
@ -480,14 +469,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.20.3"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
jni:
dependency: transitive
description:
@ -500,10 +481,10 @@ packages:
dependency: transitive
description:
name: jni_flutter
sha256: "7b717011ea40d04fd47c2731d3d1d36eb99eba3435c2753d62489e8c3c9991d5"
sha256: b2310cdd4c18c65c081ab141a41efa94aa26c65431803703ece51996f174f351
url: "https://pub.dev"
source: hosted
version: "1.0.2"
version: "1.0.3"
jni_util:
dependency: transitive
description:
@ -580,10 +561,10 @@ packages:
dependency: "direct main"
description:
name: lucide_icons_flutter
sha256: "234155b10641b8ef7bab8a077b93ea6c92fe849c06740cf89dcd86dcf350934f"
sha256: "50a21003cfe094e33b87e780d62ed5be84c32273dec1ba04547aab7df3cba00c"
url: "https://pub.dev"
source: hosted
version: "3.1.15"
version: "3.1.20"
matcher:
dependency: transitive
description:
@ -600,6 +581,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.13.0"
material_ui:
dependency: transitive
description:
name: material_ui
sha256: "134ab2f0843545c4e661e4ba2753fe1bcf0753cb690d5a9ee4870638a40411e9"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
meta:
dependency: transitive
description:
@ -608,38 +597,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
objective_c:
dependency: transitive
description:
name: objective_c
sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e
sha256: ad56fd53a78ff6b1472fa59ff2a4e8b8ccabafc586fc263a1dfad0b99b5553e3
url: "https://pub.dev"
source: hosted
version: "9.5.0"
version: "9.6.0"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "3.0.0"
path:
dependency: transitive
description:
@ -700,18 +673,18 @@ packages:
dependency: transitive
description:
name: petitparser
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
sha256: "2f1564aadb14215e478563b5406cb80729d022158c01dc50c96013c2f7eb4790"
url: "https://pub.dev"
source: hosted
version: "7.0.2"
version: "7.1.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
sha256: a36d119c13416516a7b5913fbe8af8531e11633d784c550b2125f76c758524ec
url: "https://pub.dev"
source: hosted
version: "3.1.6"
version: "3.2.0"
plugin_platform_interface:
dependency: transitive
description:
@ -720,14 +693,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.8"
pool:
dependency: transitive
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.2"
posix:
dependency: transitive
description:
@ -740,26 +705,26 @@ packages:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
sha256: "261236774e8b1d69cfc6b9eabbc96c40f25e7a2d6b171f3385d4f65d5734fb24"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
version: "2.2.1"
record_use:
dependency: transitive
description:
name: record_use
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
sha256: "1cb8564af8d43b464294411db9217f5ec04891c6f22ee2c32d73ae05e88a6bd2"
url: "https://pub.dev"
source: hosted
version: "0.6.0"
version: "1.1.1"
riverpod:
dependency: transitive
description:
name: riverpod
sha256: "84351b3472a447f7b89f961f95c73287009d58c59d8cc1267425d995f78e50f1"
sha256: "484dfc873ea4c4f4240e5635444fa066f87b07862b7279ebd004a2b71fba4b7a"
url: "https://pub.dev"
source: hosted
version: "3.4.2"
version: "3.4.3"
shared_preferences:
dependency: "direct main"
description:
@ -772,18 +737,18 @@ packages:
dependency: transitive
description:
name: shared_preferences_android
sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7"
sha256: "1e12aafe408aa50da80edfd679a2a6bf63ba7ab37c7fa98286da459a757b3399"
url: "https://pub.dev"
source: hosted
version: "2.4.27"
version: "2.4.28"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
sha256: "2ec3934efa51e46117f23031cc141b8fc878e8525b94ec1ea4f7f586cf1b47ea"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
version: "2.5.7"
shared_preferences_linux:
dependency: transitive
description:
@ -816,38 +781,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
simple_gesture_detector:
dependency: transitive
description:
@ -861,22 +794,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.13"
source_span:
dependency: transitive
description:
@ -889,10 +806,10 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
sha256: "277654b3034d17ac6f9f1cb5595db011b1d5d41e8806866db28e0abaa101c490"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
version: "1.12.2"
state_notifier:
dependency: transitive
description:
@ -921,10 +838,10 @@ packages:
dependency: "direct main"
description:
name: table_calendar
sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982"
sha256: f276347cad425ef837a41e8d9ad43f3ee7d59227aa4c36d7430607a5a18fa3b3
url: "https://pub.dev"
source: hosted
version: "3.2.0"
version: "3.2.1"
term_glyph:
dependency: transitive
description:
@ -933,14 +850,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test:
dependency: transitive
description:
name: test
sha256: ca578dc12bb8b2f40b67b7d3bd2fac4f31c01a6ff7130a14e2597b919934507f
url: "https://pub.dev"
source: hosted
version: "1.31.1"
test_api:
dependency: transitive
description:
@ -949,14 +858,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.12"
test_core:
dependency: transitive
description:
name: test_core
sha256: d2e98ec12998368dc59ddd47ab709f2cd55acd6b66dc7db764455a44082f4bc5
url: "https://pub.dev"
source: hosted
version: "0.6.18"
timezone:
dependency: "direct main"
description:
@ -985,34 +886,34 @@ packages:
dependency: transitive
description:
name: url_launcher_android
sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32
sha256: "611e87fb320b70d1dd721dc46af89c98aceccea9b31fde49e084591414e0c610"
url: "https://pub.dev"
source: hosted
version: "6.3.32"
version: "6.3.33"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
sha256: "8faa1aab294f1ab4040b43660c887b0418d5fa4f0cffef76a484e6aa1092eb4a"
url: "https://pub.dev"
source: hosted
version: "6.4.1"
version: "6.4.2"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a
sha256: "10f86fef4c2c43563fa6c211ff9cf757adf4d3ab762c56bd430664a947d70cd0"
url: "https://pub.dev"
source: hosted
version: "3.2.2"
version: "3.2.3"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
sha256: "5e835a3b869c2d70325349c81c5a45c28e20791265b67b2669da6b08c5cd5201"
url: "https://pub.dev"
source: hosted
version: "3.2.5"
version: "3.2.6"
url_launcher_platform_interface:
dependency: transitive
description:
@ -1033,10 +934,10 @@ packages:
dependency: transitive
description:
name: url_launcher_windows
sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f"
sha256: "6c5ad3f22cd4c38e089b81963b3cd7bb83b111b2df5dce008bb066162f42e429"
url: "https://pub.dev"
source: hosted
version: "3.1.5"
version: "3.1.6"
uuid:
dependency: transitive
description:
@ -1057,18 +958,10 @@ packages:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "15.3.0"
web:
dependency: transitive
description:
@ -1077,38 +970,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
win32:
dependency: transitive
description:
name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
sha256: a0b93865d5644f11cf6a8c3f6db909f1ec168958b5805f6cc684adea957cd63d
url: "https://pub.dev"
source: hosted
version: "5.15.0"
version: "6.4.0"
windows_file_picker:
dependency: transitive
description:
name: windows_file_picker
sha256: "9f3aa833068b09e380fdc59560ad260a4a14e9397173abb533699f967443602e"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
xdg_directories:
dependency: transitive
description:
@ -1121,18 +998,18 @@ packages:
dependency: transitive
description:
name: xml
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
url: "https://pub.dev"
source: hosted
version: "6.6.1"
version: "7.0.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea
url: "https://pub.dev"
source: hosted
version: "3.1.3"
version: "3.1.4"
sdks:
dart: ">=3.12.0 <4.0.0"
flutter: ">=3.44.0"
dart: ">=3.13.0 <4.0.0"
flutter: ">=3.47.0"

View file

@ -4,7 +4,7 @@ description: "CRM for everyone."
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: "none" # Remove this line if you wish to publish to pub.dev
version: 1.9.0+13
version: 1.10.0+14
environment:
sdk: ^3.8.1
@ -13,44 +13,44 @@ dependencies:
flutter:
sdk: flutter
http: ^1.1.0
google_sign_in: ^7.1.1
http: ^1.6.0
google_sign_in: ^7.2.0
shared_preferences: ^2.5.5
# Tokens live here, never in shared_preferences. Keystore-backed on Android,
# Keychain on iOS. See lib/services/token_storage.dart.
flutter_secure_storage: ^10.3.1
flutter_secure_storage: ^11.2.0
jwt_decoder: ^2.0.1
# Firebase / Crashlytics (Android only, no iOS GoogleService-Info.plist yet)
firebase_core: ^4.12.1
firebase_crashlytics: ^5.2.6
firebase_core: ^4.15.0
firebase_crashlytics: ^5.4.0
# Not referenced by name anywhere in lib/, but SwitchListTile.adaptive in
# deals_list_screen.dart pulls in Flutter's Cupertino library, whose retained
# CupertinoIcons constants need this font present. Dropping it builds fine and
# then renders a missing-glyph box. Keep it.
cupertino_icons: ^1.0.9
# Navigation
go_router: ^17.2.3
go_router: ^18.0.1
# State Management
flutter_riverpod: ^3.3.1
flutter_riverpod: ^3.4.3
# Charts
fl_chart: ^1.2.0
# Calendar
table_calendar: ^3.2.0
table_calendar: ^3.2.1
# Utilities
intl: ^0.20.2
url_launcher: ^6.3.1
intl: ^0.20.3
url_launcher: ^6.3.2
# Attachment picking. Chosen over image_picker because a CRM attaches
# contracts and PDFs at least as often as photos, and this covers both.
file_picker: ^11.0.3
file_picker: ^13.1.0
# Google Fonts for distinctive typography
google_fonts: ^8.1.0
lucide_icons_flutter: ^3.1.15
google_fonts: ^8.2.1
lucide_icons_flutter: ^3.1.20
# The org's calendar day for form defaults (lib/services/org_date.dart).
# Publisher labs.dart.dev. The `all` database, because `latest` and
@ -67,6 +67,7 @@ dev_dependencies:
# rules and activating additional ones.
flutter_lints: ^6.0.0
flutter_launcher_icons: "^0.14.4"
cross_file: ^0.3.5+5
flutter_launcher_icons:
android: "launcher_icon"

View file

@ -0,0 +1,59 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:cross_file/cross_file.dart';
import 'package:file_picker/file_picker.dart';
/// A picked file for tests.
///
/// `PlatformFile` is abstract since file_picker 12, and the real subclass is
/// built from a platform-channel map. This mirrors `AndroidPlatformFile`, the
/// one that ships: [size] is what the picker reported (0 means it did not say),
/// and `length()` falls back to measuring [bytes] or the file at [path].
///
/// [size] can disagree with the content on purpose, so a limit test can
/// report 25 MB for a file that holds three bytes.
final class FakePlatformFile extends PlatformFile {
FakePlatformFile({
required this.name,
this.size = 0,
String? path,
this.bytes,
}) : uri = path == null ? Uri(path: name) : Uri.file(path);
@override
final String name;
@override
final Uri uri;
final int size;
final Uint8List? bytes;
@override
XFile get xFile => bytes != null
? XFile.fromData(bytes!, name: name)
: XFile(path ?? name, name: name);
@override
int? lengthSync() => size > 0 ? size : null;
@override
Future<int?> length() async {
if (size > 0) return size;
if (bytes != null) return bytes!.length;
final onDisk = path;
if (onDisk == null) return null;
try {
return await File(onDisk).length();
} catch (_) {
return null;
}
}
@override
Future<Uint8List> readAsBytes() => xFile.readAsBytes();
@override
Stream<Uint8List> readAsByteStream() => xFile.openRead();
}

View file

@ -6,6 +6,7 @@ import 'package:bottle_crm/services/attachment_upload.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import '../helpers/fake_platform_file.dart';
/// The app could delete an attachment it had no way to create.
///
@ -53,7 +54,7 @@ void main() {
/// the boundary test from writing 25 MB on every run.
PlatformFile file({String name = 'quote.pdf', int size = 2048}) {
final onDisk = File('${tempDir.path}/$name')..writeAsBytesSync([1, 2, 3]);
return PlatformFile(name: name, size: size, path: onDisk.path);
return FakePlatformFile(name: name, size: size, path: onDisk.path);
}
setUp(() {
@ -188,12 +189,34 @@ void main() {
final result = await pickAndUploadAttachment(
target: AttachmentTarget.task,
recordId: 'task-1',
pickFile: () async => PlatformFile(name: 'ghost.txt', size: 10),
pickFile: () async => FakePlatformFile(name: 'ghost.txt', size: 10),
);
expect(result.succeeded, isFalse);
expect(client.sent, isNull);
});
test(
'a pick whose size cannot be measured is reported, not sent',
() async {
// file_picker 13 returns null from length() when the picker reported no
// size and the copy cannot be read, so the limit could not be checked.
final result = await pickAndUploadAttachment(
target: AttachmentTarget.task,
recordId: 'task-1',
pickFile: () async => FakePlatformFile(
name: 'gone.pdf',
path: '${tempDir.path}/does-not-exist.pdf',
),
);
expect(
result.error,
'That file could not be read. Try picking it again.',
);
expect(client.sent, isNull);
},
);
});
group('when the server refuses', () {

View file

@ -7,6 +7,7 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import '../helpers/fake_platform_file.dart';
/// Answers each request with the next queued reply and keeps what was sent,
/// so a test can check the wire as well as the state.
@ -32,7 +33,7 @@ const _csv = 'first_name,last_name\nAda,Lovelace\nAlan,Turing\n';
PlatformFile _file({String name = 'people.csv', String content = _csv}) {
final bytes = Uint8List.fromList(utf8.encode(content));
return PlatformFile(name: name, size: bytes.length, bytes: bytes);
return FakePlatformFile(name: name, size: bytes.length, bytes: bytes);
}
String _previewBody({
@ -261,7 +262,7 @@ void main() {
test('a file the picker reports as over 5 MB, before reading it', () async {
final bytes = Uint8List.fromList(utf8.encode(_csv));
await notifier().choose(
PlatformFile(name: 'big.csv', size: 6 * 1024 * 1024, bytes: bytes),
FakePlatformFile(name: 'big.csv', size: 6 * 1024 * 1024, bytes: bytes),
);
expect(
@ -275,7 +276,7 @@ void main() {
test('bytes over 5 MB when the picker did not know the size', () async {
final bytes = Uint8List(csvImportMaxBytes + 1);
await notifier().choose(
PlatformFile(name: 'big.csv', size: 0, bytes: bytes),
FakePlatformFile(name: 'big.csv', size: 0, bytes: bytes),
);
expect(stateOf().error, contains('CSV files must be 5 MB or smaller'));
@ -286,7 +287,7 @@ void main() {
client.replies.add((200, _previewBody()));
final bytes = Uint8List(csvImportMaxBytes);
await notifier().choose(
PlatformFile(name: 'edge.csv', size: bytes.length, bytes: bytes),
FakePlatformFile(name: 'edge.csv', size: bytes.length, bytes: bytes),
);
expect(stateOf().error, isNull);

View file

@ -5,10 +5,10 @@ import 'package:bottle_crm/config/api_config.dart';
import 'package:bottle_crm/data/models/support_ticket.dart';
import 'package:bottle_crm/providers/support_provider.dart';
import 'package:bottle_crm/services/api_service.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import '../helpers/fake_platform_file.dart';
Map<String, dynamic> ticketJson({
List<Map<String, dynamic>> messages = const [],
@ -109,7 +109,7 @@ void main() {
subject: 'Export failed',
category: SupportCategory.technical,
body: 'Attached.',
attachment: PlatformFile(
attachment: FakePlatformFile(
name: 'error.txt',
size: file.lengthSync(),
path: file.path,

View file

@ -4,11 +4,11 @@ import 'dart:typed_data';
import 'package:bottle_crm/providers/csv_import_provider.dart';
import 'package:bottle_crm/services/api_service.dart';
import 'package:bottle_crm/widgets/forms/csv_import_sheet.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import '../helpers/fake_platform_file.dart';
class _QueueClient extends http.BaseClient {
final List<(int, String)> replies = [];
@ -96,7 +96,7 @@ void main() {
context,
target,
onImported: () => refreshed++,
pickFile: () async => PlatformFile(
pickFile: () async => FakePlatformFile(
name: 'people.csv',
size: bytes.length,
bytes: bytes,