Skip to content

Release v1.1.0

Choose a tag to compare

@rfivesix rfivesix released this 12 Aug 02:31
· 457 commits to main since this release

Added

  • "What's New" Sheet After an Update: On the first launch after an update the app now shows a bottom sheet with the highlights of the versions the user has not seen yet, and the same list is reachable at any time from About Train Libre → What's New. The texts are authored per store locale in metadata/whats_new/<locale>.md and compiled into lib/features/whats_new/domain/whats_new_content.g.dart by script/build_whats_new.py, which also pushes them into the ## Release Notes section of metadata/app_store/<locale>.md so the in-app and App Store wording cannot drift apart. Which release notes are due is decided by comparing the marketing version from PackageInfo (build number ignored, so a new CI build does not re-trigger the sheet) against whats_new_last_seen_version in SharedPreferences; a skipped update shows every release in between, notes for a version newer than the running build stay hidden, fresh installs are seeded silently so onboarding is not followed by a third greeting, and the stored version is only written once the sheet has been dismissed. script/deploy_release.sh now runs build_whats_new.py --check before it builds anything and asks for confirmation if the notes for the version in pubspec.yaml are missing, incomplete or out of sync with the store metadata.
  • Live Activity & Dynamic Island for Workouts (iOS 16.2+): Real-time workout tracking on the Lock Screen and Dynamic Island (iPhone 14 Pro and newer), showing the active exercise, set position, weight/reps or RIR, and rest countdown without unlocking the device. Includes interactive iOS 17+ controls (-15s, +15s, Skip, 1-tap set completion) and native rest timer sound rescheduling (UNUserNotificationCenter).
  • Home Screen & Lock Screen Widgets (iOS 18+):
    • Last Workout (4x2 Medium & 4x4 Large): Displays duration, total volume, sets, and a high-resolution 2D/3D muscle heatmap visualization.
    • Muscle Readiness / Recovery (4x2 Medium & Lock Screen): 1:1 visual match of the app's muscle recovery hub, featuring compact inline text (.accessoryInline) and a 3-row aligned Lock Screen layout (.accessoryRectangular).
    • Steps 7-Day Chart (4x2 Medium): 7-day step bar chart with goal reference line, completion checkmark badges, and daily step count.
    • Body Measurements (4x2 Medium Configurable): Sparkline trend chart with selectable timeframes (7 Days, 1 Month, 3 Months, 6 Months, Max) and selectable metrics (Weight, Body Fat, Circumferences).
    • Today's Glance / Nutrition (4x2 Medium & Lock Screen): Today's nutrition summary, circular protein gauge (.accessoryCircular), and clean Lock Screen calorie & protein breakdown.
    • Quick Actions & Lock Screen Buttons: 1-Tap action buttons for Barcode Scanner, AI Meal Capture, Start Workout, Add Water, Log Supplement, Add Measurement, and Add Food.
  • iOS App Shortcuts, Siri & Control Center Controls (iOS 18+ / Action Button): All 7 quick actions are available as native iOS App Shortcuts (Siri voice triggers, Shortcuts app, Spotlight search), Control Center buttons (ControlWidgetButton), Lock Screen bottom controls, and hardware Action Button mappings (iPhone 15 Pro / 16). Instant launch of the barcode scanner without delay.
  • Automatic Exercise Navigation on Live Workout Launch: Opening a live workout automatically scrolls to the exercise holding the next open set directly below the summary bar.

Changed

  • Startup Prompts Are Serialized: The app tour, the release notes and the review prompt were previously fired side by side from MainScreen's post-frame callback, which only ever worked because the tour and the review prompt rarely became due on the same launch. They now run one after the other with at most one prompt per launch, in the order app tour → "What's New" → review prompt, so an update — the one moment when several of them can be due at once — cannot stack three sheets on top of each other.
  • Reduced Redundant Iteration Passes in Hot Paths: Replaced chained .where()/.map()/.reduce()/.fold() pipelines with single-pass for loops in several frequently-executed code paths, cutting allocations and repeated traversals of the same collection: pulse summary metric extraction (PulseRepository._summaryFromBuckets), resting pulse median calculation (PulseRepository._restingFromAggregateBuckets, PulseAnalysisEngine._restingPulse), and live workout completion stats (LiveWorkoutViewModel.finishWorkout). No behavioral change; output is identical to the previous implementation.

Fixed

  • Onboarding Telemetry Initial Step Tracking: Fixed an issue where the initial step (welcome, step_index: 0) of OnboardingScreen was omitted from PostHog telemetry because tracking relied exclusively on PageView.onPageChanged, which does not fire on initial render. Added explicit post-frame tracking for step 0 so onboarding funnels begin at step 0 instead of skipping to step 1 (unit_system).
  • Database Update Loading Screen Freeze & Performance Bottleneck: Fixed the database update progress screen hanging for 15-30+ seconds at 100% ("all entries present"). Resolved a major query bottleneck in RetainHistoricalOffProductsUseCase.execute that previously fetched all 26 columns of 125,000 product rows into Dart memory as full Drift DataClasses instead of querying only product barcodes, cutting post-processing execution time from ~20 seconds to ~30 milliseconds. Additionally, progress calculation across batch imports (BasisDataManager._performBatchImport) was scaled accurately (0-70% main exercise import, 70-100% exercise relational translation pass; 0-95% OFF product import, 95-100% OFF retention cleanup), streaming continuous progress updates and localized detail text so the UI progress bar never stalls at 100%.
  • iCloud Sync Emitted feature_used Telemetry on Every Background Sync: Fixed ICloudSyncService._snapshotAndUpload emitting FeatureKey.icloudSyncTriggered to PostHog every time the app was backgrounded with iCloud sync enabled. Routine background syncs no longer transmit telemetry events. Telemetry is now only emitted when the user explicitly enables or disables the feature (setting_toggled for icloud_sync_enabled, plus feature_used for icloud_sync_triggered when enabled) or performs a manual backup ("Backup Now").
  • Keyboard-Aware Rest Bar: The floating rest bar automatically hides when the software keyboard opens to avoid covering input fields.
  • No Catalog Download Was Offered After Restoring a Backup: Restoring a JSON backup onto a fresh install left the app convinced it already had the wger exercise and Open Food Facts catalogs while both tables were empty, so the download prompt never appeared and the catalog screens stayed blank. Catalog installation is tracked purely in SharedPreferences, and BackupManager exported every preference key and rewrote all of them verbatim after prefs.clear() on import — but the JSON payload carries no catalog rows, only user data. The restored device therefore inherited the source device's installed_training_version, installed_off_version_*, is_exercise_catalog_initialized and last_db_sync_app_version. Everything downstream trusted those keys: isOffDatabaseInitialized() / isExerciseCatalogInitialized() read nothing but preferences, and every screen that gates the prompt on them (food explorer, scanner, add-food, exercise catalog, AI capture) saw "ready"; the empty-table self-heal inside checkForBasisDataUpdate was itself gated on !isExerciseInitialized, so a restored true flag skipped the very check written to catch this; and a matching last_db_sync_app_version made the base-food and category sync return early. Fixed on both sides. A new CatalogStatePrefs (lib/core/infrastructure/catalog_state_prefs.dart) enumerates the keys that describe what is on this disk; backups no longer export them, the importer skips any that older backups still contain, and the device's own values are carried across prefs.clear() so restoring onto a device that does have the catalogs still re-downloads nothing. Independently, BasisDataManager no longer takes the preferences at face value: both isInitialized checks confirm the rows exist (memoized once verified, so the hot screen paths do not re-query), the exercise health check runs unconditionally, base foods and categories are verified the same way, and an OFF version claim that no rows back up is cleared so the prompt is offered instead of a phantom "up to date". Covered by regression tests for the export filter, both import directions, and the two presence checks.
  • iCloud Restore Left the App Reading the Pre-Restore Database: downloadAndRestore renamed the downloaded snapshot over app_hybrid.sqlite while the live Drift connection was still open, leaving that connection bound to the now-unlinked old inode; the caller closed it only afterwards, via DatabaseHelper.driftDb?.close(), which never cleared the static — so dbInstance kept handing out the closed database, and the UnitService.reload() and _loadAdaptiveGoalSettings() calls immediately following the restore ran against it. Stale -wal/-shm sidecars belonging to the previous database were also left beside the restored file, where SQLite would attempt to recover from them. The restore now closes and clears the connection itself before the swap (new DatabaseHelper.closeAndResetDriftDb()), deletes the sidecars, then extracts the bundled preferences and drops BasisDataManager's catalog-presence memo; the onboarding caller no longer closes anything on its own.
  • Missing Tooltip on Onboarding Back Button: Added a localized tooltip (MaterialLocalizations.of(context).previousPageTooltip) to the icon-only back button in the onboarding flow, so screen readers announce its purpose instead of just "button".

Security

  • AI API Keys Were Included in iOS Device and iCloud Backups: BYOK provider keys are stored in the Keychain via FlutterSecureStorage and correctly never appear in the app's own backups — neither the JSON export (BackupManager reads only SharedPreferences, which the keys never touch) nor the iCloud snapshot (VACUUM INTO of the Drift database plus _bundleSharedPreferences). Apple's backups were the leak: const FlutterSecureStorage() defaults to KeychainAccessibility.unlocked (kSecAttrAccessibleWhenUnlocked), which — unlike the …ThisDeviceOnly classes — is captured by iOS device and iCloud backups and restored onto whatever device the backup is applied to, so a user's OpenAI/Gemini/Anthropic key left the device with every backup. AiService now writes every item it owns (ai_api_key_*, ai_selected_provider, ai_selected_model_*, ai_custom_base_url, ai_custom_model, ai_timeout_seconds) with KeychainAccessibility.unlocked_this_device, exposed as AiService.deviceOnlySecureStorage; LocalAppDataResetService shares the same instance, since kSecAttrAccessible is part of the keychain lookup query and a mismatch would make "delete all local data" silently miss the items. Existing installs are migrated by AiService.migrateSecureStorageToDeviceOnly(), run once from main() before anything reads storage: for each key it reads with the legacy options, deletes, then rewrites device-only. The delete is required rather than a plain overwrite because SecItemAdd rejects a duplicate service/account pair while a new-options read cannot see the old item; the plugin's own delete path clears accessibilityLevel from the query, so it removes the backup-eligible item regardless. The done-flag is only set after every key succeeds, so a failure retries on the next launch instead of half-migrating. Android is unaffected: the AES key is wrapped by a hardware Keystore key that is never backed up, making any copied ciphertext undecryptable off-device.