fix(app): migrate flutter_contacts to 2.x — 1.x crashes under UIScene - #11595
Conversation
1.1.9+2's iOS registration force-unwraps UIApplication.shared.delegate!.window!!.rootViewController!, which is nil under the UIScene lifecycle (BasedHardware#11568) and crashes with EXC_BREAKPOINT on launch. Plugin registration is alphabetical and synchronous (GeneratedPluginRegistrant.m), so this crash takes down every alphabetically later plugin too — SharedPreferences, Sqflite, PermissionHandler, Geolocator, WebView, ~27 in total on this project. Confirmed via full crash backtrace on iPhone 17 Pro / iOS 27.0 while verifying the UIScene migration. No 1.x patch exists past 1.1.9+2 (checked the full pub.dev version list); the fix only ever landed in 2.x, which is a complete rewrite of the plugin's Dart-facing API, not a drop-in bump. Migrated the 3 call sites that use it: - requestPermission({readonly}) -> bool => permissions.request(PermissionType.read|readWrite) -> PermissionStatus (granted/limited now stand in for the old `true`) - getContacts(withProperties: true, withPhoto: false) => getAll(properties: {ContactProperty.phone}) — narrower and cheaper, since these call sites only ever read phones/displayName, and those two are always fetched regardless of the properties set requested - Contact.displayName: non-null String -> nullable String? in 2.x - Phone.label: bare PhoneLabel enum -> Label<PhoneLabel> wrapper, phone.label.name -> phone.label.label.name phone_calls_page.dart and phone_call_provider.dart both already import permission_handler, which also exports a type named PermissionStatus — writing flutter_contacts' bare PermissionStatus in either file is an ambiguous-import error. Fixed by hiding permission_handler's PermissionStatus in both imports; neither file spells that package's own PermissionStatus by name anywhere (only via `var status = await Permission.contacts.status`), so nothing else changes. Bonus: 2.3.1 ships a Package.swift, so flutter_contacts drops off the "doesn't support Swift Package Manager" warning Flutter prints on every iOS build. Verified: flutter analyze clean (0 errors, 1 pre-existing unrelated lint). Combined with the UIScene migration and a Firebase local-config fix on a throwaway community-build test, the app reached the sign-in screen on iPhone 17 Pro / iOS 27.0, confirmed visually — the first time this plugin registration chain has completed under UIScene on this project. No dedicated regression test: there's no way to unit-test a native UIScene-launch crash without device access. The version constraint itself (`^2.3.1`, commented in pubspec.yaml) is the guard — a downgrade to 1.x would silently reintroduce the crash. Failure-Class: none Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
2 issues found across 5 files
Confidence score: 2/5
- In
app/lib/pages/conversation_detail/widgets/share_to_contacts_sheet.dart, the read-only contact picker requestsWRITE_CONTACTSalongsideREAD_CONTACTSon Android, and because onlyREAD_CONTACTSis declared the permission flow can fail and block contact loading for users — request onlyREAD_CONTACTSfor this path (or declare/write-gateWRITE_CONTACTSwhere truly needed). - In
app/lib/pages/phone_calls/phone_calls_page.dart, usingphone.label.label.namecan show the enum token (custom) instead of the user’s real custom label, which degrades contact clarity in call UI — read the display value fromcustomLabelwhen the label type is custom.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="app/lib/pages/conversation_detail/widgets/share_to_contacts_sheet.dart">
<violation number="1" location="app/lib/pages/conversation_detail/widgets/share_to_contacts_sheet.dart:78">
P1: On Android, this read-only flow requests `WRITE_CONTACTS` in addition to `READ_CONTACTS`, but the app declares only `READ_CONTACTS`. The request therefore returns denied and prevents users from loading contacts; request `PermissionType.read` instead.</violation>
</file>
<file name="app/lib/pages/phone_calls/phone_calls_page.dart">
<violation number="1" location="app/lib/pages/phone_calls/phone_calls_page.dart:284">
P3: For phones with custom labels, `phone.label.label.name` renders the literal enum name ('custom') instead of the contact's actual label. Since `Label<PhoneLabel>` carries the display text in `customLabel`, prefer reading it so custom-labeled numbers show the real label (e.g. 'Business') rather than the generic 'custom'. This preserves the prior 1.x string, but the migrated accessor is the place to use the richer value.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
|
|
||
| // Request contacts permission using flutter_contacts' own method | ||
| final permissionGranted = await FlutterContacts.requestPermission(); | ||
| final status = await FlutterContacts.permissions.request(PermissionType.readWrite); |
There was a problem hiding this comment.
P1: On Android, this read-only flow requests WRITE_CONTACTS in addition to READ_CONTACTS, but the app declares only READ_CONTACTS. The request therefore returns denied and prevents users from loading contacts; request PermissionType.read instead.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/lib/pages/conversation_detail/widgets/share_to_contacts_sheet.dart, line 78:
<comment>On Android, this read-only flow requests `WRITE_CONTACTS` in addition to `READ_CONTACTS`, but the app declares only `READ_CONTACTS`. The request therefore returns denied and prevents users from loading contacts; request `PermissionType.read` instead.</comment>
<file context>
@@ -75,7 +75,8 @@ class _ShareToContactsBottomSheetState extends State<ShareToContactsBottomSheet>
// Request contacts permission using flutter_contacts' own method
- final permissionGranted = await FlutterContacts.requestPermission();
+ final status = await FlutterContacts.permissions.request(PermissionType.readWrite);
+ final permissionGranted = status == PermissionStatus.granted || status == PermissionStatus.limited;
</file context>
| final status = await FlutterContacts.permissions.request(PermissionType.readWrite); | |
| final status = await FlutterContacts.permissions.request(PermissionType.read); |
| initial: contact.displayName.isNotEmpty ? contact.displayName[0].toUpperCase() : '?', | ||
| onCall: () => _makeCall(phone.number, contactName: contact.displayName), | ||
| name: name, | ||
| phone: '${phone.label.label.name} ${phone.number}', |
There was a problem hiding this comment.
P3: For phones with custom labels, phone.label.label.name renders the literal enum name ('custom') instead of the contact's actual label. Since Label<PhoneLabel> carries the display text in customLabel, prefer reading it so custom-labeled numbers show the real label (e.g. 'Business') rather than the generic 'custom'. This preserves the prior 1.x string, but the migrated accessor is the place to use the richer value.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/lib/pages/phone_calls/phone_calls_page.dart, line 284:
<comment>For phones with custom labels, `phone.label.label.name` renders the literal enum name ('custom') instead of the contact's actual label. Since `Label<PhoneLabel>` carries the display text in `customLabel`, prefer reading it so custom-labeled numbers show the real label (e.g. 'Business') rather than the generic 'custom'. This preserves the prior 1.x string, but the migrated accessor is the place to use the richer value.</comment>
<file context>
@@ -276,11 +278,12 @@ class _PhoneCallsPageState extends State<PhoneCallsPage> with SingleTickerProvid
- initial: contact.displayName.isNotEmpty ? contact.displayName[0].toUpperCase() : '?',
- onCall: () => _makeCall(phone.number, contactName: contact.displayName),
+ name: name,
+ phone: '${phone.label.label.name} ${phone.number}',
+ initial: name.isNotEmpty ? name[0].toUpperCase() : '?',
+ onCall: () => _makeCall(phone.number, contactName: name),
</file context>
kodjima33
left a comment
There was a problem hiding this comment.
flutter_contacts 1.x force-unwraps rootViewController and takes down the whole plugin registration chain under UIScene; 2.x migration is the only fix, and all 3 call sites are migrated correctly.
## What Every Codemagic mobile build has been failing at `Get Flutter packages` since 15 Aug: ``` The current Dart SDK version is 3.11.5. Because flutter_contacts 2.3.1 requires SDK version ^3.12.0 and no versions of flutter_contacts match >2.3.1 <3.0.0, flutter_contacts ^2.3.1 is forbidden. So, because omi depends on flutter_contacts ^2.3.1, version solving failed. ``` Bumps the eight Codemagic Flutter pins from `3.41.9` to `3.44.5`, matching what GitHub Actions already runs. ## Why it broke Two commits, three days apart, neither wrong on its own: - **`35c146d3fa`** (12 Aug, *stabilize mobile and Swift release CI*) moved **GitHub Actions** to Flutter 3.44.5 and left **Codemagic** on 3.41.9. From then on the two lanes ran different Flutter versions. - **`34e2ba8e72`** (15 Aug, #11595, *migrate flutter_contacts to 2.x*) bumped `flutter_contacts: ^1.1.9+2` → `^2.3.1`, which requires Dart `^3.12.0`. It touched `pubspec.yaml`, `pubspec.lock` and three call sites — correctly — and nothing else. | Lane | Flutter | Dart | Result | |---|---|---|---| | GitHub Actions | 3.44.5 | 3.12.2 | passes | | Codemagic | 3.41.9 | 3.11.5 | `version solving failed` | **CI could not have caught this.** #11595 was green on GitHub Actions because that lane was already on a Flutter new enough to satisfy the new constraint. The failure only appears on the lane whose version nothing checks. ## Scope All eight workflows with a Flutter pin were affected — every internal and store build path: `ios-internal-auto`, `android-internal-auto`, `ios-prod-testflight`, `macos-prod-appstore`, `android-prod-internal`, `ios-prod-patch`, `macos-prod-legacy-no-publish`, `android-prod-patch` Also updated the developer-facing copies of the same pin, which would otherwise hand a new contributor a toolchain that cannot resolve this repo: `app/setup.sh`, `app/setup/scripts/setup.ps1`, `docs/doc/developer/AppSetup.mdx`, `docs/rayban-meta-dat-setup.md`, `docs/rayban-meta-founder-acceptance.md`. Left alone: `app/ios/test/rayban_dat_plugin_boundary_test.rb:233`, where `'version' => '3.41.9'` is arbitrary fixture content inside a fake `.flutter-plugins-dependencies` blob for a plugin-registration test, not a toolchain pin. `xcode`, `cocoapods` and `java` pins are unchanged. ## Why 3.44.5 and not 3.47.0 `pub` suggests 3.47.0, but 3.44.5 is the version GitHub Actions already runs green on this exact `pubspec.lock`. Matching the lane that works is the smaller change; moving both lanes to a newer Flutter is a separate decision with its own verification. ## Verification Reproduced and fixed locally on the real toolchains, against this repo's unmodified `pubspec.yaml`/`pubspec.lock`: - Flutter **3.41.x** (Dart 3.11.0): `flutter pub get` fails with the identical `version solving failed` error. - Flutter **3.44.5** (Dart 3.12.2, confirmed via `dart --version`): `flutter pub get` → `Got dependencies!`, and `git status` is clean afterwards, so the lockfile does not drift. - `app/test.sh` on 3.44.5 at this branch's base — **1338 passed**, whole suite green on the toolchain this PR moves Codemagic to. - `codemagic.yaml` parses as YAML and all 8 pins read `3.44.5`; asserted programmatically rather than by eye. - `make preflight` — 11/11. Not verified: the Codemagic build itself. I cannot trigger one, so the first real proof is the next build after merge. The failing step is `pub get`, which is exactly what was reproduced above. ## Follow-up (deliberately not in this PR) Nothing checks that `codemagic.yaml` and the GitHub workflow pins agree, which is the actual reason a routine dependency bump took out every release lane while CI stayed green. A manifest check comparing the two would have failed #11595 in CI instead of at build time. Left out to keep this PR to the unblock; happy to add it separately.
Every Codemagic mobile build has failed at `Get Flutter packages` since 15 Aug: The current Dart SDK version is 3.11.5. Because flutter_contacts 2.3.1 requires SDK version ^3.12.0 [...] version solving failed. Two commits, three days apart, neither wrong on its own. 35c146d moved GitHub Actions to Flutter 3.44.5 and left Codemagic on 3.41.9, so the two lanes began running different Flutter versions. 34e2ba8 (BasedHardware#11595) then bumped flutter_contacts to ^2.3.1, which requires Dart ^3.12.0 — fine on 3.44.5 (Dart 3.12.2), impossible on 3.41.9 (Dart 3.11.5). CI could not have caught it: BasedHardware#11595 was green because the GitHub lane was already new enough, and nothing checks the lane that was not. All eight pinned workflows were affected — every internal and store build path. Also updates the developer-facing copies of the same pin (setup.sh, setup.ps1, AppSetup.mdx, the two rayban docs), which would otherwise hand a new contributor a toolchain that cannot resolve this repo. The '3.41.9' left in rayban_dat_plugin_boundary_test.rb is fixture content inside a fake .flutter-plugins-dependencies blob, not a toolchain pin. xcode, cocoapods and java pins are unchanged. 3.44.5 rather than the 3.47.0 pub suggests: 3.44.5 is what GitHub Actions already runs green on this exact lockfile, so matching the working lane is the smaller change. Moving both lanes to a newer Flutter is its own decision. Verification, against this repo's unmodified pubspec.yaml/pubspec.lock: Flutter 3.41.x (Dart 3.11.0) — `flutter pub get` reproduces the identical version-solving failure. Flutter 3.44.5 (Dart 3.12.2, per `dart --version`) — `flutter pub get` prints "Got dependencies!" and leaves `git status` clean, so the lock does not drift. codemagic.yaml parses as YAML and all 8 pins read 3.44.5, asserted in code rather than by eye. make preflight — 11/11. Not verified: a real Codemagic run; I cannot trigger one. The failing step is `pub get`, which is what was reproduced above.
Why
flutter_contacts1.1.9+2's iOS registration force-unwrapsUIApplication.shared.delegate!.window!!.rootViewController!, which is nil under the UIScene lifecycle (#11568) and crashes withEXC_BREAKPOINTon launch. Plugin registration is alphabetical and synchronous (GeneratedPluginRegistrant.m), so this crash takes down every alphabetically later plugin too —SharedPreferences,Sqflite,PermissionHandler,Geolocator,WebView, ~27 in total. Confirmed via full crash backtrace on iPhone 17 Pro / iOS 27.0 while verifying the UIScene migration.What changed
No 1.x patch exists past 1.1.9+2 — checked the full pub.dev version list. The fix only landed in 2.x, a complete Dart-API rewrite. Migrated the 3 call sites:
requestPermission({readonly})→boolbecomespermissions.request(PermissionType.read|readWrite)→PermissionStatus(granted/limitedstand in for the oldtrue).getContacts(withProperties: true, withPhoto: false)becomesgetAll(properties: {ContactProperty.phone})— narrower and cheaper, since these call sites only ever read phones/displayName, and those two are always fetched regardless of the requested property set.Contact.displayName: non-nullString→ nullableString?in 2.x.Phone.label: barePhoneLabelenum →Label<PhoneLabel>wrapper;phone.label.name→phone.label.label.name.phone_calls_page.dartandphone_call_provider.dartboth already importpermission_handler, which also exports a type namedPermissionStatus— writingflutter_contacts' barePermissionStatusin either file is an ambiguous-import error. Fixed withhide PermissionStatuson thepermission_handlerimport in both files; neither file spells that package's ownPermissionStatusby name anywhere, so nothing else changes.Bonus: 2.3.1 ships a
Package.swift, soflutter_contactsdrops off the "doesn't support Swift Package Manager" warning Flutter prints on every iOS build.Verification
flutter analyze: 0 errors on the 3 changed files (1 pre-existing unrelated lint), 0 new errors project-wide. Combined with the UIScene migration (#11570) and a Firebase local-config fix (separate PR) on a throwaway community-build test, the app reached the sign-in screen on iPhone 17 Pro / iOS 27.0, confirmed visually — the first time this plugin registration chain has completed under UIScene on this project.No dedicated regression test: there's no way to unit-test a native UIScene-launch crash without device access. The version constraint itself (commented in
pubspec.yaml) is the guard — a downgrade to 1.x would silently reintroduce the crash.Failure-Class: none