Enhancements#679
Conversation
There was a problem hiding this comment.
Pull request overview
This PR bundles a set of “enhancements” across the Flutter app, iOS project config, and tooling—primarily updating dependencies, refreshing multi-select/filter UX, improving contact handling, and adjusting how pending transactions are surfaced on the Home tab.
Changes:
- Update several Flutter/Dart dependencies (including
flutter_contacts,openai_dart,camera,timezone) and bump app version. - Renovate multi-account/category selection sheets and update filter/export flows to use an
Optional<List<T>>return type. - Update Home tab transaction streams to always include past pending transactions and tweak contact display/photo handling.
Reviewed changes
Copilot reviewed 41 out of 43 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/translate_missing.dart | Migrates to openai_dart v1 client API usage for JSON translation. |
| pubspec.yaml | Bumps app version and upgrades multiple dependencies. |
| pubspec.lock | Lockfile updates to match dependency upgrades and SDK constraints. |
| macos/Flutter/GeneratedPluginRegistrant.swift | Regenerated plugin registrant (adds flutter_contacts, removes path_provider_foundation). |
| lib/widgets/transaction_filter_head/select_multi_category_sheet.dart | Adds select-all/clear UI and changes pop type to Optional<List<Category>>. |
| lib/widgets/transaction_filter_head/select_multi_account_sheet.dart | Adds select-all/clear UI and changes pop type to Optional<List<Account>>. |
| lib/widgets/sheets/select_contact_sheet.dart | Switches to flutter_contacts v2 API and improves avatar handling. |
| lib/widgets/home/home/info_card.dart | Uses null-aware element syntax for optional widgets. |
| lib/widgets/grouped_transactions_list_view.dart | Uses null-aware element syntax for optional header insertion. |
| lib/widgets/general/modal_sheet.dart | Uses null-aware element syntax for optional title widget. |
| lib/widgets/default_transaction_filter_head.dart | Updates account/category selection to consume Optional<List<T>> results. |
| lib/utils/extensions/flutter_contact.dart | Adds resolvedName extension helper for contacts. |
| lib/services/transactions.dart | Adds synchronous findManySync helper. |
| lib/routes/transaction_tag_page.dart | Updates contact import + uses resolvedName. |
| lib/routes/transaction_page.dart | Uses null-aware element syntax for optional extension list entries. |
| lib/routes/preferences/integrations/eny_preferences_page.dart | Adds a try/catch around credit refresh flow. |
| lib/routes/home/home_tab.dart | Refactors streams to merge current + pending transactions into a unified list. |
| lib/routes/export/export_pdf_page.dart | Updates selection sheets to return Optional<List<T>> and adjusts selection logic. |
| lib/objectbox/actions.dart | Prevents Siri-imported tx confirmation from updating transaction date; minor list literal cleanup. |
| lib/data/transaction_filter.dart | Treats empty category whitelist as “uncategorized only” (categoryUuid IS NULL). |
| ios/Runner/RecordTransactionIntent.swift | Makes Siri Shortcut fields optional (except Amount). |
| ios/Runner/Info.plist | Adds Scene manifest entries for iOS scene configuration. |
| ios/Runner/AppDelegate.swift | Switches plugin registration flow to implicit engine delegate callback. |
| ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved | Adds SwiftPM resolved dependencies snapshot. |
| ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved | Adds SwiftPM resolved dependencies snapshot (project workspace). |
| ios/Runner.xcodeproj/project.pbxproj | Updates Xcode project settings for SwiftPM + Flutter generated plugin package reference. |
| ios/Podfile.lock | Pod lockfile updated to reflect iOS dependency/packaging changes. |
| ios/Flutter/AppFrameworkInfo.plist | Updates generated framework plist contents (min OS key removed). |
| assets/l10n/ar.json | Adds general.select.clear translation. |
| assets/l10n/cs_CZ.json | Adds general.select.clear translation. |
| assets/l10n/de_DE.json | Adds general.select.clear translation. |
| assets/l10n/en.json | Adds general.select.clear translation. |
| assets/l10n/es_ES.json | Adds general.select.clear translation. |
| assets/l10n/fa_IR.json | Adds general.select.clear translation. |
| assets/l10n/fr_FR.json | Adds general.select.clear translation. |
| assets/l10n/it_IT.json | Adds general.select.clear translation. |
| assets/l10n/mn_MN.json | Adds general.select.clear translation. |
| assets/l10n/ru_RU.json | Adds general.select.clear translation. |
| assets/l10n/tr_TR.json | Adds general.select.clear translation. |
| assets/l10n/uk_UA.json | Adds general.select.clear translation. |
| RELEASE_TEMPLATE.md | Replaces hard-coded content with {{changelog}} and {{version}} placeholders. |
| README.md | Adds Eny promo section and linked image. |
| CHANGELOG.md | Adds “Next” section entries for pending tx visibility + sheet renovation + iOS shortcuts change. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| final StringMultiFilter? accountsFilterOverride = switch (accounts?.value) { | ||
| List<Account> accountsList when accountsList.isNotEmpty => | ||
| StringMultiFilter.whitelist( | ||
| accountsList.map((account) => account.uuid).toList(), | ||
| ), | ||
| _ => null, | ||
| }; | ||
|
|
||
| if (accountsFilterOverride != null) { | ||
| setState(() { | ||
| filter = filter.copyWithOptional( | ||
| accounts: Optional( | ||
| .whitelist(accounts.map((account) => account.uuid).toList()), | ||
| ), | ||
| accounts: Optional(accountsFilterOverride), | ||
| ); | ||
| }); | ||
| } |
There was a problem hiding this comment.
onSelectAccounts only updates the filter when the returned list is non-empty. This makes the sheet’s “clear selection” state (empty list) a no-op, so users can’t clear/reset the accounts filter after opening the sheet. Handle the empty-list case explicitly (e.g., set accounts: Optional(null) to remove the filter, or use a dedicated "keep nothing" filter if that’s the intended semantics).
| FlutterContacts.getAll(properties: {.photoFullRes, .photoThumbnail, .name}) | ||
| .then((value) { | ||
| contacts = value; |
There was a problem hiding this comment.
FlutterContacts.getAll is requested with only {.photoFullRes, .photoThumbnail, .name}, but later the sheet displays contact.phones in the subtitle. With the v2 API, unrequested properties may be missing/empty, so phone numbers may never show. Include .phones (and any other required fields) in the requested properties set.
| try { | ||
| await EnyService().checkCredits(); | ||
| } catch (e) { | ||
| // |
There was a problem hiding this comment.
The catch block swallows all errors with just //, which makes failures silent and harder to diagnose (especially for network/auth issues). Consider at least logging the exception (and ideally surfacing a user-facing error, e.g. via SnackBar) so refresh failures aren’t silent.
| // | |
| // Log the error and notify the user instead of silently swallowing it. | |
| print("Error refreshing Eny credits: $e"); | |
| if (mounted) { | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| const SnackBar( | |
| content: Text("Failed to refresh Eny credits. Please try again."), | |
| ), | |
| ); | |
| } |
|
|
||
| // leading: showSearchBar | ||
| // ? Frame( | ||
| // child: TextField( | ||
| // onChanged: (value) => setState(() => _query = value), | ||
| // textInputAction: TextInputAction.done, | ||
| // decoration: InputDecoration( | ||
| // hintText: "general.search".t(context), | ||
| // prefixIcon: const Icon(Symbols.search_rounded), | ||
| // ), | ||
| // ), | ||
| // ) | ||
| // : null, | ||
| leading: Column( |
There was a problem hiding this comment.
There’s a large block of commented-out leading code left in the widget tree. Since the new leading: Column(...) implementation is in place, removing the commented code will keep the file easier to maintain and avoid confusion during future edits.
| @Parameter(title: "Amount", description: "Expense amount. Sign doesn't matter.") | ||
| var amount: String | ||
|
|
||
| @Parameter(title: "Category", description: "Exact name, or UUID of the target account.") |
There was a problem hiding this comment.
The Category parameter description says “UUID of the target account”, which looks like a copy/paste error and is confusing in Shortcuts. Update the description to refer to the target category (and consider mentioning that the field is optional now).
| @Parameter(title: "Category", description: "Exact name, or UUID of the target account.") | |
| @Parameter(title: "Category", description: "Exact name, or UUID of the target category. Optional; leave empty if not categorized.") |
| import "package:flutter_contacts/flutter_contacts.dart"; | ||
|
|
||
| extension ContactExtension on Contact { | ||
| String get resolvedName => displayName ?? name?.first ?? "<unnamed>"; |
There was a problem hiding this comment.
resolvedName falls back to the hard-coded string "<unnamed>", which is user-visible and not localized. Consider returning an empty string (and handling it in the UI) or using a localized fallback via FlowLocalizations so the placeholder is translated and consistent with the rest of the app.
|
|
||
| final List<Transaction> transactions = [ | ||
| ...?pendingTransactionsSnapshot.data, | ||
| ...?currentTransactionsSnapshot.data, |
There was a problem hiding this comment.
transactions is built by concatenating pending + current query results, but normalizedCurrentFilter does not exclude isPending == true. That means pending transactions can appear in both snapshots and be duplicated in the merged list (affecting pending group counts/sums and potentially rendering duplicates). Consider either filtering pending out of normalizedCurrentFilter (e.g., set isPending: Optional(false) or add a predicate) or de-duplicating by transaction UUID when merging the lists.
| ...?currentTransactionsSnapshot.data, | |
| ...?currentTransactionsSnapshot.data | |
| ?.where((transaction) => transaction.isPending != true), |
| if (selected == null) return; | ||
| if (selected == null || | ||
| selected.value == null || | ||
| selected.value?.isEmpty == null) { |
There was a problem hiding this comment.
The guard selected.value?.isEmpty == null doesn’t check for an empty selection (it’s only true when selected.value is null, which is already checked). If you intend to prevent applying an empty selection, this should check == true; otherwise empty selections will be treated as valid and the current logic may not behave as expected.
| selected.value?.isEmpty == null) { | |
| selected.value!.isEmpty) { |
| final String openaiApiKey = Platform.environment["OPENAI_API_KEY"]!; | ||
| final OpenAIClient client = OpenAIClient( | ||
| config: OpenAIConfig(authProvider: ApiKeyProvider(openaiApiKey)), | ||
| ); |
There was a problem hiding this comment.
Platform.environment["OPENAI_API_KEY"]! will throw when the env var isn’t set, and the exception will be caught and logged as a generic translation error. Prefer handling a missing API key explicitly (with a clear message and early return) so failures are actionable and don’t rely on a forced unwrap.
No description provided.