Skip to content

fix(app): migrate flutter_contacts to 2.x — 1.x crashes under UIScene - #11595

Merged
kodjima33 merged 1 commit into
BasedHardware:mainfrom
formed2forge:fix/flutter-contacts-uiscene-crash
Aug 15, 2026
Merged

fix(app): migrate flutter_contacts to 2.x — 1.x crashes under UIScene#11595
kodjima33 merged 1 commit into
BasedHardware:mainfrom
formed2forge:fix/flutter-contacts-uiscene-crash

Conversation

@formed2forge

@formed2forge formed2forge commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Why

flutter_contacts 1.1.9+2's iOS registration force-unwraps UIApplication.shared.delegate!.window!!.rootViewController!, which is nil under the UIScene lifecycle (#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. 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})bool becomes permissions.request(PermissionType.read|readWrite)PermissionStatus (granted/limited stand in for the old true).
  • getContacts(withProperties: true, withPhoto: false) becomes 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 requested property set.
  • Contact.displayName: non-null String → nullable String? in 2.x.
  • Phone.label: bare PhoneLabel enum → Label<PhoneLabel> wrapper; phone.label.namephone.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 with hide PermissionStatus on the permission_handler import in both files; neither file spells that package's own PermissionStatus by name anywhere, 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.

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

Review in cubic

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>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 requests WRITE_CONTACTS alongside READ_CONTACTS on Android, and because only READ_CONTACTS is declared the permission flow can fail and block contact loading for users — request only READ_CONTACTS for this path (or declare/write-gate WRITE_CONTACTS where truly needed).
  • In app/lib/pages/phone_calls/phone_calls_page.dart, using phone.label.label.name can show the enum token (custom) instead of the user’s real custom label, which degrades contact clarity in call UI — read the display value from customLabel when 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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}',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 kodjima33 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@kodjima33
kodjima33 merged commit 34e2ba8 into BasedHardware:main Aug 15, 2026
30 checks passed
mdmohsin7 added a commit that referenced this pull request Aug 16, 2026
## 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.
ZachL111 pushed a commit to ZachL111/omi that referenced this pull request Aug 16, 2026
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.
@cursor
cursor Bot deleted the fix/flutter-contacts-uiscene-crash branch September 2, 2026 02:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants