From ecd1e6f12136918eb13ffb6211bdd6ce765bf8ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?An=C4=B1lcan=20=C3=87ak=C4=B1r?= Date: Thu, 25 Jun 2026 02:56:55 +0300 Subject: [PATCH 1/3] fix: keep Pick.saveFile source-compatible with file_picker 12 file_picker 12 made FilePicker.saveFile's fileName and bytes required and non-null, breaking the analyzer build under a fresh pub get that resolves the newer file_picker (master CI is currently red on this). Pick.saveFile keeps its nullable facade surface but guards both arguments before forwarding, so it type-checks against file_picker 11 and 12 and fails with a clear ArgumentError on null instead of an unhelpful type error. --- CHANGELOG.md | 1 + lib/src/facades/pick.dart | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc6c1c3..b2ed511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes to this project will be documented in this file. ### Fixed +- **`Pick.saveFile` is source-compatible with file_picker 12.** file_picker 12 made `FilePicker.saveFile`'s `fileName` and `bytes` parameters required and non-null, which broke the analyzer build (`argument_type_not_assignable`) under a fresh `flutter pub get` that resolved the newer file_picker. `Pick.saveFile` keeps its nullable facade surface but now guards both arguments before forwarding, so the call type-checks against file_picker 11 and 12 and a null argument fails with a clear `ArgumentError` instead of an unhelpful type error. Touches `lib/src/facades/pick.dart`. - **`Crypt` now accepts the `base64:` app key that `key:generate` produces.** `key:generate` writes `APP_KEY=base64:`, but `EncryptionServiceProvider` required `app.key` to be a raw 32-character string and threw `App Key must be 32 characters for AES-256` on the generated key, so `Crypt.encrypt`/`decrypt` were unusable out of the box. Added `MagicEncrypter.fromAppKey(appKey)` which base64-decodes a `base64:`-prefixed key to its 32 bytes (and still accepts a raw 32-character key); `EncryptionServiceProvider` now binds through it. Touches `lib/src/encryption/magic_encrypter.dart`, `lib/src/encryption/encryption_service_provider.dart`; adds three `fromAppKey` cases to `test/encryption/magic_encrypter_test.dart`. - **`MagicStatefulView` now calls the controller's `onInit()` lifecycle hook.** `MagicStatefulViewState.initState` listened to the controller and called the VIEW's own `onInit()` hook, but never invoked the CONTROLLER's `onInit()`, despite the documented contract. A controller that bootstraps in `onInit` (initial data load, table creation, subscriptions) silently never ran it when backed by a `MagicStatefulView`, so the screen rendered against uninitialized state (e.g. a query against a table the controller's `onInit` was supposed to create). It now calls `_controller.onInit()` guarded by `MagicController.initialized`, so a `SimpleMagicController` that already initialized in its constructor is not double-initialized and a singleton controller reused across re-mounts initializes exactly once per lifetime. Touches `lib/src/ui/magic_view.dart`; adds `test/ui/magic_view_controller_oninit_test.dart`. - **Auth no longer warns on every boot of a fresh app.** `AuthServiceProvider.boot()` logged a `userFactory not registered` warning (blaming provider order) whenever no userFactory was set, even for apps with no stored session to restore. It now only warns when a stored session actually exists (`Auth.hasToken()`) but cannot be rebuilt; a fresh app or a logged-out user stays quiet (debug-level). The stored-session check is guarded so a misconfigured Auth (for example, no Vault registered) cannot crash boot from this warning-verbosity path. Touches `lib/src/auth/auth_service_provider.dart`; adds three cases to `test/auth/auth_test.dart`. diff --git a/lib/src/facades/pick.dart b/lib/src/facades/pick.dart index 3377e41..84ffc02 100644 --- a/lib/src/facades/pick.dart +++ b/lib/src/facades/pick.dart @@ -363,6 +363,13 @@ class Pick { String? fileName, Uint8List? bytes, }) async { + // file_picker 12 made fileName and bytes required and non-null on + // FilePicker.saveFile; guard here so the nullable facade surface stays + // source-compatible across file_picker 11 and 12 and fails with a clear + // error instead of an unhelpful type error. + if (fileName == null || bytes == null) { + throw ArgumentError('Pick.saveFile requires both fileName and bytes.'); + } return FilePicker.saveFile( dialogTitle: dialogTitle, fileName: fileName, From 8baa6aa7d908e17d821ef49390c03aeb1da347bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?An=C4=B1lcan=20=C3=87ak=C4=B1r?= Date: Thu, 25 Jun 2026 03:13:34 +0300 Subject: [PATCH 2/3] test: cover Pick.saveFile null guard --- test/facades/pick_test.dart | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 test/facades/pick_test.dart diff --git a/test/facades/pick_test.dart b/test/facades/pick_test.dart new file mode 100644 index 0000000..3d49bbe --- /dev/null +++ b/test/facades/pick_test.dart @@ -0,0 +1,23 @@ +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:magic/src/facades/pick.dart'; + +void main() { + group('Pick.saveFile null guard', () { + test('throws ArgumentError when fileName is null', () { + expect( + () => Pick.saveFile(bytes: Uint8List.fromList([1, 2, 3])), + throwsArgumentError, + ); + }); + + test('throws ArgumentError when bytes is null', () { + expect(() => Pick.saveFile(fileName: 'report.pdf'), throwsArgumentError); + }); + + test('throws ArgumentError when both are null', () { + expect(() => Pick.saveFile(), throwsArgumentError); + }); + }); +} From 55dd3b3c9f0e70235173e63590fba3b311d15a26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?An=C4=B1lcan=20=C3=87ak=C4=B1r?= Date: Thu, 25 Jun 2026 03:35:32 +0300 Subject: [PATCH 3/3] docs: clarify Pick.saveFile fileName and bytes are required --- lib/src/facades/pick.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/facades/pick.dart b/lib/src/facades/pick.dart index 84ffc02..27ec2b6 100644 --- a/lib/src/facades/pick.dart +++ b/lib/src/facades/pick.dart @@ -349,8 +349,8 @@ class Pick { /// /// **Parameters:** /// - [dialogTitle]: Title of the save dialog. - /// - [fileName]: Default file name. - /// - [bytes]: Optional bytes to write to the file. + /// - [fileName]: File name to save as. Required; throws [ArgumentError] if null. + /// - [bytes]: Bytes to write to the file. Required; throws [ArgumentError] if null. /// /// ```dart /// final savePath = await Pick.saveFile(