# App Ideas: Should the iOS and Android apps be unified with Flutter or React Native?

*Written 2026-07-16, based on a review of `connect-ios/` and `connect-android/`.*

## What the apps actually are

Both apps are the same product on two platforms, built natively:

| | iOS (`connect-ios/`) | Android (`connect-android/`) |
|---|---|---|
| Size | ~5,800 lines Swift | ~6,600 lines Kotlin |
| Shell | WKWebView (`WebViewController`, `ExtWebViewController`, `ProjectWebViewController`) | WebView (`MainActivity`, `ProjectWebviewActivity`, `RemoteWebviewActivity`) |
| Meetings | LiveKit Swift SDK 2.0.14 (`MeetingViewController` ~1,660 lines + `ParticipantCell`) | LiveKit Android SDK 2.20.1 + CameraX (`MeetingActivity` + `MeetingViewModel`, ~1,200 lines, XML/ViewBinding) |
| Incoming calls | CallKit + PushKit VoIP push (`CallDelegate`, `AppDelegate`) | Self-managed Telecom `ConnectionService` + FCM (`CallConnectionService`, `CallHandler`, `IncomingCallActivity`, `ForegroundService`) |
| Branding | Multiple targets (API Connect, API Team) + alternate icon sets (BMWNA, BMWCA, RRMC, Fields) | Gradle product flavors (`connect`, `team`, `bmwdealerforum`) + icon activity-aliases |
| Auth | ssotoken cookie injected into the webview cookie store | Same (`cookieNames` sync in `MainActivity`) |
| JS bridge | `WKScriptMessageHandler`: loaded/error, launchProject, updateBadge, updateIcon, print, callOutgoing*, callEnded | `@JavascriptInterface`: androidPrint, launchProject, launchMeeting, callEnded |
| Crash/analytics | Firebase Crashlytics/Analytics | Sentry + Firebase Messaging |

The important observation: **the UI is already unified — it lives in the PHP web app.** The native code is almost entirely platform *integration*: cookie/SSO plumbing, push registration, incoming-call plumbing, downloads/print/file chooser, icon switching, fallback/timeout handling — plus one genuinely duplicated screen, the LiveKit meeting room.

## What a cross-platform rewrite would and wouldn't buy

### Would unify well
- **The meeting room UI.** This is the biggest real duplication (~1,900 lines iOS + ~1,200 lines Android of participant grid, controls, chat overlay, audio-device handling). LiveKit ships official SDKs for both frameworks (`livekit_client` for Flutter, `@livekit/react-native`), both actively maintained, both with a components library. One implementation instead of two is a real win.
- **The webview shell logic.** Cookie injection, JS bridge, timeout/fallback, badge/icon messaging — the *logic* could be written once (`flutter_inappwebview` or `react-native-webview`). But note this code is already written, debugged, and full of hard-won edge-case handling (file chooser, downloads, print, keyboard behavior). Rewriting it means re-fighting those battles inside a plugin's abstraction.

### Would NOT unify
- **Incoming calls.** This is the highest-risk area. CallKit/PushKit on iOS and self-managed `ConnectionService` on Android are inherently per-platform. In Flutter/RN you'd depend on community plugins (`flutter_callkit_incoming`, `react-native-callkeep`) that are notoriously fragile — and VoIP push on iOS has hard OS rules (report a call to CallKit on every push or get killed/banned from VoIP push). Today this code uses first-party APIs directly; a rewrite trades that for a third-party plugin dependency, and likely still requires maintaining custom native code around it. **Net negative.**
- **Branding/flavors.** Gradle flavors and Xcode targets/schemes/icon sets stay native either way. Flutter/RN "flavors" are wrappers over exactly what already exists.
- **Push registration, notification channels, foreground services, dynamic app icons, document interaction** — all remain platform-specific glue behind plugin boundaries.

### Honest accounting
Of ~12,400 lines of native code, maybe 40–50% is logic that would genuinely become shared code. The other half becomes either plugin configuration or *retained native code called through a plugin bridge* — same code, one more layer. And a rewrite resets the maturity clock on two shipping, stable apps for zero user-visible improvement.

## Flutter vs React Native, if we did it

**Flutter would be the pick.** Reasons specific to us:

- Our web stack is PHP + jQuery, not React. React Native's main draw — reusing React skills and components from the web team — does not apply here.
- LiveKit's Flutter SDK is more mature and more actively developed than the RN one; the RN SDK also depends on `react-native-webrtc`, adding another layer.
- Flutter's tooling (single binary toolchain, no Metro/node_modules churn) suits a small team; dependency upgrades in RN projects are a recurring tax.
- `flutter_inappwebview` is considerably more capable than `react-native-webview` (cookie manager, download/file-chooser hooks, print) — closest match to what the shells do today.

RN would only make sense if we planned to move the web frontend to React and wanted one JS codebase end-to-end. Kotlin Multiplatform isn't a fit either — there's almost no shared *business* logic to extract; the duplication is UI and OS glue, which KMP shares least.

## Recommendation

**Don't do a full Flutter/RN rewrite.** These apps are the worst-case profile for cross-platform payoff: thin webview shells whose native substance is exactly the platform-specific code (CallKit/Telecom, VoIP push, flavors) that cross-platform frameworks handle worst. The cost is a multi-month rewrite plus permanent dependence on community call-integration plugins; the benefit is deduplicating roughly one screen and some shell logic.

Two cheaper directions that capture most of the value:

1. **Move the meeting room into the web app (biggest win, no rewrite).** LiveKit has a first-class JS SDK, and WKWebView has supported getUserMedia/WebRTC since iOS 14.5 (our floor is already there in practice; the Podfile's iOS 12 target is worth revisiting regardless — LiveKit Swift requires 13+). If the meeting UI ran in the existing webview, ~3,000 lines of duplicated native meeting code disappear, meetings ship changes without app-store review, and desktop browsers get the same room for free. The native apps shrink to shell + call-notification plumbing. Caveats to spike first: CallKit/Telecom audio-session interaction with webview WebRTC, backgrounding behavior during calls, and echo cancellation/device-picker quality vs the native SDKs.

2. **Converge the two shells behind one contract.** Even staying native, we can define a single documented JS-bridge spec (message names, cookie set, lifecycle events) shared by both apps — the two bridges have already drifted (`launchMeeting` exists on Android only; iOS handles more message types). One spec file in `docs/` plus periodic conformance checks keeps the apps behaving identically where it matters, at near-zero cost.

If a rewrite is ever forced (e.g., we lose the ability to maintain Swift *and* Kotlin), choose Flutter, keep the call-notification layer as hand-written platform code from day one, and do option 1 first anyway — a webview-meetings app is a far smaller thing to rewrite.
