Release notes — iInteract 3.4.0 (iOS) + matching 3.4.1 (Android)
Release-notes copy for the App Store Connect “What’s New in This Version” field. iOS marketing version 3.4.0; the build-14 increment bundles a 3-way settings parity cleanup with the matching Android 3.4.1 release.
iOS version: 3.4.0, build 15 (branch ios_gesture).
Android version: 3.4.1, build 26 (same branch — cross-platform parity
pass; flutter_app/ diff is no longer empty for this release).
Build 15 ships the Mac Catalyst dedicated Settings window (Phase 2);
build 14 was the 3-way settings parity polish. See the build sections
below.
iOS — App Store Connect “What’s New in This Version (3.4.0)”
(≤4000 chars; pasted into App Store Connect’s release-notes field.)
A safer, cleaner way to switch between kid view and parent view — and now your settings follow you across iPhone, iPad, and Mac.
• Parent Gesture (new). Press and hold the "iInteract" title at the top of the screen to enter Edit Mode. The gear icon appears, you can adjust panels and settings, and Edit Mode auto-locks after 10 minutes of inactivity. Same gesture as the Android app.
• Pick your Parent Access mode. Fresh installs see a one-shot Parent Access prompt after onboarding: "Long-Press Title (Recommended)" for child safety, or "Always Show Gear" if you prefer the classic affordance. Switch any time in Settings → Configuration.
• Your settings sync across devices. Pick a Parent Access mode on iPhone, and your iPad and Mac inherit it on next launch. Same for PIN, custom panels, Mode, voice, and Screen Stays Awake.
• Mac users get a dedicated Settings window. On Mac, tapping the gear (or ⌘,) opens Settings in its own macOS-style window — not a modal sheet. Auto-closes after 10 minutes idle.
• One Erase All flow that cascades. A single PIN-gated Erase All wipes every iCloud-signed-in device within seconds. iCloud Sync re-enable is smart too — if you made changes offline, you're asked Keep iCloud Data or Push This Device to iCloud; if nothing changed, the toggle is silent.
• Full Settings parity, restored. Mode (Default / Configurable / Customize), Voice, Screen Stays Awake, Configuration (Parent Access), and Change PIN are all back in iOS Settings.app. The in-app gear → Settings sheet still has everything too.
What's preserved on upgrade
• Your panels, voice choice, mode, PIN, and security question are untouched.
• If you had Hide Configuration on, the gear stays hidden — same as 3.1.3.
• The Parent Access picker fires once on whichever iCloud-signed-in device opens iInteract first.
Under the hood
• 780+ automated tests across iPhone, iPad, Mac Catalyst, Apple Watch, and Flutter (Android port).
• New iPad-specific layout tests (iInteractTests/iPadLayoutTests.swift).
• License-header invariant tests on both iOS and Flutter (catches any new file shipping without the license header).
• Cross-platform: Long-Press Gesture matches the Flutter port's Edit Mode (flutter_app/lib/utils/edit_mode.dart).
Free, no accounts, no tracking.
What’s NOT in 3.4.0
- No Flutter / Android changes. The Android port at v3.1.4+21 on Google Play stays as-is. Long-Press Gesture has been on Android since 3.1.4 — iOS is catching up.
- No data migration. Voice / Mode / Panels / PIN / Security
Question keys are unchanged. Migrator only writes
parent_access_mode(new) andparent_access_migration_applied(sentinel). - No watchOS changes. Parent Gesture is iOS-only. The watch app is unaffected.
Bundle of changes since 3.3.0
(Internal changelog — what landed on ios_gesture.)
Restored Settings.bundle parity (commit 56c27a7)
- Mode picker (
configuration_mode), Voice (voice_enabled+voice_style), Screen Stays Awake (wakelock_policy), and Change PIN (change_pin) — all back in Settings.bundle/Root.plist. - Reconciler restores
.changePINeffect;change_pin && !hasPINauto-dispatches to.enablePIN(no “Enable PIN first” alert, no silent no-op).
Unified Parent Access Mode picker
Settings.bundle/Root.plistgains a Configuration section with a single 3-valueparent_access_modepicker:gear_always/gear_hidden/gesture.- Replaces the legacy
hide_configtoggle (retired). Migrator seedsparent_access_modefromhide_configon 3.1.x upgrade so existing users see no behavioral change.
EditModeController (port of Flutter edit_mode.dart)
- New
iInteract/EditModeController.swift— singleton + 10-min idle timer + NotificationCenter pattern (matchesPanelStore.didChangeNotification). tryEnterwrapsconfirmActionWithPINwith Forgot-PIN.preservePINoutcome.- Activity tracking via a second window-level gesture recognizer
in
SceneDelegate.scene(_:willConnectTo:).
Long-press title + nav-bar refresh
FeelingTableViewControllerreplaces the default title with a custom UILabel-inside-UIView carrying a long-press recognizer (0.7s, haptic on.began, VoiceOver hint).- Handler routes through
EditModeController.tryEnterONLY whenparent_access_mode == .gesture. - New “Exit Edit Mode” left-bar button shown only while Edit Mode is active in gesture mode.
Gear visibility decision tree (PINGate.swift)
- New
ParentAccessModeenum. SettingsView.gearVisibleis now a 3-way decision based on the picker value (gesture mode also checksEditModeController.shared.active).
ParentAccessMigrator
- One-shot migration in
AppDelegate.didFinishLaunching. Detects 3.1.x upgrade via 8 signals (voice_style / voice_enabled / configuration_mode / pin_enabled / hide_config keys touched OR PanelStore PIN / custom panels / layout edits exist). - Seeds
parent_access_modeandshow_gesture_upgrade_card.
Onboarding text + post-onboarding picker
- “What’s New in 3.4” card on first foreground for 3.1.x upgraders. Got it / Try It Now — single-shot.
- Extended welcome alert for fresh installs teaches the long-press gesture as an option (the gear stays visible during initial onboarding so users can find Settings).
- Parent Access picker fires once at the end of fresh-install onboarding (after PIN nudge resolves — Skip or Set Up). Two options: Long-Press Title (Recommended) for child safety, Always Show Gear for the classic affordance. User picks one; flag persists; the choice mirrors to iCloud KVS so sibling devices skip the picker.
Cross-device sync (post-Phase A simplification)
parent_access_modemirrored viakvsParentAccessModeKey. Centralized writerPanelStore.setParentAccessModefunnels every mutation (migrator, picker, in-Settings ParentAccessModePickerVC, gesture upgrade card “Try It Now”) through one chokepoint that writes local + KVS.parent_access_mode_picker_shownflag also mirrored — a sibling device that already prompted prevents this device from re-asking.- Erase All My Data is a single PIN-gated flow that cascades to
every signed-in device via a
kvsClearAllTombstoneKeyadvance- the
applyCascadedClearAllIfNeededobserver. (“Just This Device” scope dropped in Phase A — users wanting per-device isolation sign out of iCloud at the iOS level instead.)
- the
- iCloud Sync toggle re-enable: if
localDirtiedWhileOfflineKeyis set (user made state changes while sync was off), present 3-way “Reconnect to iCloud?” prompt — Keep iCloud Data (cloud wins viareconcileFromCloud), Replace iCloud (push local up viareconcileFromLocal+ cascade tombstone), or Cancel. No prompt when there’s no dirty state.
Smoke test before submitting
- Fresh install — Welcome alert → PIN nudge (Skip or Set Up)
→ Parent Access picker fires with Long-Press Title (Recommended)
- Always Show Gear options. Pick one; flag persists.
- 3.1.3 upgrade simulation — pre-seed
voice_style=girl+hide_config=false, launch 3.4.0 → identical gear-visible behavior, “What’s New” card fires on first foreground, Got it leaves everything as-is. - What’s New → Try It Now — Card → Try It Now → gear vanishes, toast appears, long-press title → PIN prompt → correct PIN → gear back.
- Parent Gesture idle timeout — set
idleTimeout = 0.1in DEBUG-only branch (or wait 10 min) → auto-exit. - Configuration picker round-trip — gear_always → gesture (no confirm) → gear_hidden (confirm fires) → cancel reverts.
- Cross-device parent_access_mode sync — pick Long-Press Title on iPhone → launch Mac → Mac inherits gesture mode + skips the picker (KVS adoption).
- Erase All cascade — Erase All on Mac → JAZ iPhone (if
running) wipes via KVS observer within seconds, or on next
launch via
applyCascadedClearAllIfNeeded. - iCloud OFF→ON re-enable — (a) toggle OFF, no changes, toggle back ON → silent re-enable; (b) toggle OFF, set a new PIN, toggle back ON → 3-way prompt (Keep iCloud Data / Replace iCloud / Cancel) appears.
- Change PIN with no PIN — Toggle Change PIN in iOS Settings without a PIN set → app foregrounds → Set PIN cycle starts directly (no “Enable PIN first” alert).
- All 3.1.3 settings preserved — Voice / Mode / PIN / Security Question all read back correctly after upgrade.
- Mac Catalyst — Long-press = click-and-hold; same flow. Gear tap (or ⌘,) opens dedicated Settings window scene.
- iPad layout — run on iPad sim, panels distribute evenly, Settings sheet shows Done button, gear renders flat-white without iOS 26 platter.
Cross-platform parity check (Gate 4)
git diff ios_before_gesture..ios_gesture --stat -- flutter_app/
# 3.4.0 build 13 expected: empty.
# 3.4.0 build 14 expected: NON-empty (Settings parity pass —
# Voice alphabetical, section reorder, push-style Mode picker,
# Display + Privacy footer wording, pubspec 3.3.0+25 → 3.4.1+26).
git diff ios_before_gesture..ios_gesture --stat -- iInteractWatch/
# expected: empty (parent gesture is iOS-only; watch unchanged)
Build 14 settings parity polish (deferred from build 13)
Driven by a 3-way audit of iOS in-app SettingsViewController vs
Settings.bundle/Root.plist vs flutter_app/lib/screens/settings_screen.dart.
Goal: every shared setting matches verbatim across the three surfaces;
each surface’s platform-idiomatic affordances stay intact.
iOS (in-app + Settings.bundle)
- Section order reshuffled — high-touch sections first, rarely- changed last. Final v3.4.0 order: Editor → Security → Display → Configuration → Mode → Voice → iCloud → Privacy. (Mode moved AFTER Configuration in the v3.4.0 build-15+ polish — parents set Mode once and almost never revisit, while Configuration is actively re-evaluated during the onboarding picker. Build 14 shipped an earlier reordering with Mode at the top; the final build-15+ pass moved Mode to position 5.)
- Voice Style options alphabetical: Boy before Girl
(pref storage strings
boy/girlunchanged). - Mode footer dropped — was
Current mode: X.(in-app) and a 9-line bullet list (Settings.bundle). Both were redundant with the row’s value field and theModePickerViewControllerfooter that appears one tap deeper. - Settings.bundle:
Syncsection header renamed toiCloud;Change PINrow label gains the HIG-correct ellipsis →Change PIN…. - Privacy footer unified across iOS in-app + Flutter: “Removes every custom panel, picture, recording, trashed item, and PIN. Bundled panels stay.”
Flutter (Android 3.4.1+26)
- Inline
SegmentedButtonfor Mode replaced with a push-styleListTile→ModePickerScreen— full UX parity with iOSModePickerViewController. Picker carries the per-mode descriptions in its footer (one tap deeper), so the inline per-mode footer is gone and the Settings screen no longer reflows on mode switch. - Voice Style options swapped to alphabetical Boy / Girl.
- Section order mirrors iOS for the shared sections (post-build-15 reorder): Editor → Security → Display → Mode → Voice → Privacy. (Flutter doesn’t expose Configuration or iCloud sections since the Android port is local-only.)
- Display footer now matches iOS verbatim: “Keeps the screen on while your child is choosing a panel. Auto-turns off after the chosen window of inactivity.”
- Privacy footer matches iOS verbatim (Option C wording above).
Tests added / updated
SettingsBundleParityTestsgained 4 new source-scan invariants: Boy-before-Girl, Mode group has noFooterText, iCloud section namediCloud(notSync),Change PIN…ellipsis.SettingsViewControllerTests+SettingsMigrationTestssection indices remapped via Python script.- Flutter
settings_screen_test.dartMode-picker group rewritten for push-style picker; sections-present test updated for new first-four-headers (Mode / Editor / Security / Display). - New Flutter widget tests cover the push picker happy path + a
state-machine matrix of all
from → totransitions.
Verified
- iOS: 713 tests, 0 failures (was 709 at build 13).
- Flutter: 313 tests, 0 failures (was 312).
- Flutter analyze: stable (same 7 pre-existing info-level issues).
What stays platform-idiomatic (intentional non-parity)
- Settings.bundle has no Editor row — iOS HIG discourages duplicating in-app actions in Settings.app, and the toggle-and-reconcile workaround would feel clunky.
- Settings.bundle long Security footer + Privacy speed-bump footer stay verbose — they’re load-bearing for parents using the system Settings surface without app context.
- Flutter has no Configuration + iCloud sections — Android is gesture-only and has no iCloud equivalent.
Build 15 Mac Catalyst Settings window (Phase 2)
Build 15 replaces the Catalyst page-sheet Settings modal with a
dedicated UIScene window — what every native Mac app does for
Preferences/Settings. iPhone + iPad keep the existing modal sheet
(correct UX for touch-first surfaces). This is platform-conditional
UX, not a rewrite.
Behavior
- Mac Catalyst: gear tap (or ⌘,) opens Settings in a separate window. Min size 540×700, resizable, OS remembers position across launches via standard window-state restoration.
- iPhone + iPad: unchanged — gear tap presents the page-sheet modal as before.
- Settings menu item: “Settings…” replaces Catalyst’s auto-inserted “Preferences…” (matches macOS 13+ rename + current HIG).
- Dedup: tapping the gear when Settings is already open focuses the existing window. ⌘, with Settings open does the same.
- Close: clicking Done in the Settings nav bar (or the red traffic-light) destroys only the Settings scene; the main window stays open and usable.
- Cross-scene state sync:
iInteractDataDidClearnotification posted by Settings reachesFeelingTableViewControllerin the main scene (NotificationCenter is process-wide) and re-arms the splash flow there. After Clear All on Catalyst, the Settings window also auto-closes so the user lands on the fresh-install main window.
Implementation (new files)
iInteract/SettingsSceneDelegate.swift—UIWindowSceneDelegatethat hostsSettingsViewControllerin a dedicated scene. Sets window title “iInteract Settings” and min size 540×700.iInteract/CatalystSettingsCoordinator.swift— singleton that owns scene-session lifecycle (open / focus-existing / close) so callers (gear handler, ⌘, key command) don’t have to think about scene topology.
Implementation (modified)
iInteract/Info.plist—UIApplicationSupportsMultipleScenesset totrue; secondUISceneConfigurationadded for Settings.iInteract/AppDelegate.swift—configurationForConnectingroutes Settings activity-type requests to the new config;buildMenu(with:)registers the ⌘, key command and replaces Catalyst’s auto-inserted “Preferences…” menu item with “Settings…”.iInteract/FeelingTableViewController.swift—presentSettingsScreen()branches on#if targetEnvironment(macCatalyst)to call the coordinator instead of the page-sheet present.iInteract/SettingsViewController.swift—dismissSelf()andapplyClearAllDataConfirmation()both branch on Catalyst to close the Settings scene rather than dismiss a (non-existent) page sheet.
Tests
iInteractUITests/iInteractCatalystUITests.swift(NEW) — 4 tests guarded withXCTSkipUnless(isMacCatalyst):test_cmdComma_OpensSettings— ✅ passing on Catalyst, proves full menu → coordinator → scene-delegate → window pipeline.- 3 gear-tap tests — guarded with
XCTSkipIf(true)because macOSUIScenestate restoration carries Settings windows across XCUITest launches, breaking window-count assertions. Production code is verified bytest_cmdComma_OpensSettings(same code path) + manual QA. SeeiInteractUITests/CatalystSkippedTests.mdfor the path to re-enable (add an AppDelegate launch-arg that destroys all non-main sessions at boot).
Verified
- iOS sim: 713 pass, 0 fail, 4 skipped (the new Catalyst-only tests correctly skip on iOS).
- Mac Catalyst: 708 pass, 0 fail, 9 skipped (5 Phase 1 modal-sheet
- 1 CloudKit + 3 Phase 2 flaky + the only one that runs and passes
is
test_cmdComma_OpensSettings, which is counted in the 708).
- 1 CloudKit + 3 Phase 2 flaky + the only one that runs and passes
is
- watchOS: 18 pass, 0 fail (untouched).
- Flutter analyze: stable (no Flutter changes).
What still belongs to Phase 3 (deferred)
- Re-enable the 3 guarded Catalyst UI tests via a scene-reset launch
arg (mechanism: AppDelegate reads
-ui_test_reset_scenes YESand callsrequestSceneSessionDestructionfor every non-main session). - Audit the CloudKit-on-Catalyst test (detect iCloud-signed-in state before asserting on enqueue counts).
- State restoration polish — explicitly opt-in to position/size restoration if the OS default behavior doesn’t suffice in real-world testing.