Repository navigation
feat(bindings): add the Flutter plugin - #581
Merged
Merged
Conversation
…r for enhanced functionality
#587) * feat(bindings): Flutter events, sendMessage, and Internet transport (PR2) * chore(bindings): update Android build configuration and dependencies for Flutter integration * fix(bindings): address Flutter PR2 review (compile, Internet, hygiene) * fix(bindings): Flutter PR2 review polish (auth, lazy Internet, guards) * chore(bindings): Swift Internet bootstrap guard and example lints * refactor(bindings): remove unused library declaration from offline_protocol.dart
#588) * feat(bindings): Flutter PR3 BLE, Wi‑Fi Direct, event replay, lifecycle * fix(bindings): Flutter event session lifecycle and PR3 review follow-ups * chore(bindings): pin Flutter method channel surface and add seq reset test * chore(bindings): add Flutter plugin AAR size baseline and measure script
) * feat(bindings): Flutter CI, bridge docs, and workflow guards (PR4) * fix(bindings): harden Flutter CI pins, path filter, and single Gradle gate * fix(bindings): split Flutter CI dart vs native path filters * fix(uniffi): isolate Flutter guards and gate lib.rs on native CI * fix(bindings): trigger Flutter native CI on lockfile and engine crates * perf(bindings): skip Flutter native gate on engine-only PRs
Fixes CI Format check on feat/flutter-binding-pr1-scaffold.
CocoaPods PathList does not index symlinked RN bridge sources, so the pod rsyncs shared iOS bridge files into BridgeRN on install. Fix plugin teardown and smokeTestConfig so the example boots on device without requiring BLE background modes until BLE is actually started. Co-authored-by: Cursor <cursoragent@cursor.com>
Drop PR-scoped sections and duplicate CI narrative from integration docs. Align example and baseline READMEs with kotlin/swift/RN style; scrub PR labels from user-facing comments and baseline metadata. Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve root README install table: keep Flutter and Java rows. Co-authored-by: Cursor <cursoragent@cursor.com>
…1/0382 Dependency Audit (cargo-deny) requires patched TLS trust-store crates used by ureq. Co-authored-by: Cursor <cursoragent@cursor.com>
The workspace lock got rustls-webpki 0.103.16 and webpki-roots 1.0.10 for RUSTSEC-2026-0381 and 0382 earlier on this branch. The capi-fuzz workspace has its own lockfile, and it still pinned the vulnerable 0.103.15 and 1.0.9. Nothing gates that lockfile, so it would have sat there until somebody happened to run cargo audit inside tools/. Bump it to the same versions.
Moving the iOS config parser and the runtime config applier out
of the React Native modules was supposed to be a pure move. It
wasn't.
Every comment that names a failure went missing on the way: why
maxGroupMembers is UInt32(clamping:), why initialTtl is clamped,
why historyWindowSize is clamped in the unsigned domain, why the
data and security sections stay absent, why username discovery
and enforceAdminCommits default to false. Those comments exist so
that nobody "simplifies" the shape back into a trap. The Rust
guards strip comments before matching, so nothing noticed. Put
them back.
It also changed behaviour. The RN iOS module now passed
`context ?? [:]` to emitDiagnostic, which omits the key for nil,
so every info diagnostic from the DORS and relay path suddenly
grew a `"context": {}`. Pass the optional through.
While at it: drop readStoredConfig and startIfConfigured from
both Internet bootstraps, since nothing calls them; have the RN
Kotlin module take its battery and history bounds from the
applier instead of keeping a second copy; and get the em dashes
out of the two bootstrap parity guards' messages.
The plugin had two lists of sources to take, one in the podspec
and one in the Gradle build, with Rust guards pinning both and
the bridge doc describing them as the compile surface. Neither
did anything. The podspec never assigned its list, and the Kotlin
plugin ignores `java { include }`. The excludes were doing all the
work, and a file in neither list compiled fine. Delete the
take-lists and their guards, and guard what actually decides the
build: every excluded name must be a real file, the podspec and
the rsync copy must exclude the same sources, and no shared
transport may be excluded. That is what ADR 0025 asked for in the
first place.
The version, SDK levels, dependency versions and iOS deployment
target were all literals. ADR 0025 decision 5 exists for exactly
this. Now the podspec and Gradle build read them from the React
Native package. The pubspec can't read another file, so the
release gate checks it against the tag. Before this, 0.30.0 would
have shipped a plugin that says 0.29.0, and nothing would fail.
The native hosts had real bugs too:
- Android caught Exception, but a missing .so throws
UnsatisfiedLinkError. On the engine thread that kills the
process. Catch Throwable, report LinkageError as
NATIVE_LIBRARY_UNAVAILABLE.
- Both hosts blocked the main thread on detach, Android for up
to ten seconds. Queue the teardown instead. Android keeps one
executor that is never shut down, so a re-attach queues behind
the old teardown rather than racing it.
- iOS read eventSink from the callback thread with no lock, and
deinit did engineQueue.sync, which deadlocks if deinit runs on
that queue. Lock the sink; tear down directly in deinit.
- A config enabling Reticulum or Nostr was accepted, but there is
no manager for either. Refuse it at create.
In CI, the native path filter missed the plugin's own android
tree, so a PR touching only the plugin's Kotlin never compiled it.
Add it, and validate the example's committed Gradle wrapper before
running it. Build the CI library with the mobile profile Android
actually ships, and make the size check subtract other ABIs so a
local four-ABI tree measures what CI does. The re-measured
baseline is 5.2 MiB; the old one measured the release profile.
While at it: drop the dead branch in the Dart replay path, handle
errors on the native event stream, remove the PR numbers from
identifiers, document the stale BridgeRN copy and a manual iOS
typecheck, and add the CHANGELOG entry this binding never had.
The last round made detach queue its teardown instead of blocking the main thread on it. The comment said a later attach "queues behind that teardown on the same thread". It doesn't. It turns out that Flutter creates a *new* plugin instance for every engine, on both platforms, and each instance had its own executor on Android and its own serial queue on iOS. So after an Android activity is destroyed and relaunched, the new engine's create runs on a fresh thread while the old engine's teardown is still sitting in stop(), blocked on transport I/O. Two cores, one identity, both on BLE and the relay, and the old one free to flush protocol state over whatever the new one just loaded. The previous head didn't have this problem only because it waited on the main thread, which is the thing we were trying to stop doing. React Native doesn't have it because invalidate() tears down synchronously. Make the engine thread one per process: the executor moves into the companion object and the iOS queue becomes a static let. Every call from every instance now lines up behind the teardown, which is what the comment claimed all along. A guard refuses a per-instance thread coming back, and the bridge doc says why it has to be this way.
create() on both hosts tore down the running engine first and only then parsed the new config and checked it for transports the plugin doesn't host. So a create with nostrEnabled set got a perfectly good refusal, and also a dead engine. The app asked for one thing and lost another. Parse and refuse first, tear down only once the config is accepted. A rejected create now leaves the current session exactly as it was. While at it, make stop() on iOS report the core's failure. It wrapped everything in try? and always answered success, while Android rethrows the first teardown failure. Same failure, two different answers in Dart. The transport managers' stop() can't throw, so only the core's error is left to report, and now it is.
Two paths in the Dart event pipeline had no test at all: dropping a malformed event from the native channel, and the cap that flushes held events when a replay never answers. The second one matters. Without the cap, a native side that never answers replayEvents would hold every later event until the timer fires, and a busy session can queue a lot of events in two seconds. The cap test uses a replay that never completes, so the only thing that can release the held events is the cap itself. It checks that everything past the gap comes out in order and the gap is reported.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the Flutter plugin,
bindings/flutter/offline_protocol, for iOS and Android. This branch carries what was planned as four PRs: #587 (events,sendMessage, Internet transport), #588 (BLE, Wi-Fi Direct, event replay, lifecycle) and #589 (CI job, bridge docs, guards) were merged into it, so it is reviewed as one change.createFromConfig,initializeMlsWithSecureStorage,start,stop,destroy,localAddress,sendMessage,enableTransport/enableInternetTransport, andOfflineProtocol.eventswith the sameseqgap detection and nativereplayEventsas React Native.OfflineProtocolPlugin.ktandOfflineProtocolPlugin.swiftrun every UniFFI call on one engine thread per process, shared by every plugin instance, and answer on the main thread. Detach queues the teardown and never blocks the main thread, and the next engine's calls queue behind it.createvalidates its config before it tears down the running engine.pod installcopies them into a gitignoredBridgeRN/because CocoaPods does not follow directory symlinks.ProtocolConfigParser.swift,InitialRuntimeConfigApplier.{swift,kt},InternetTransportBootstrap.{swift,kt}.pubspec.yamlcannot read another file, so the release gate now checks it against the tag.reticulumEnabledornostrEnabledfails atcreate, since the plugin hosts no manager for either.Flutter Bindingjob runsflutter testandflutter analyze, and on native-relevant changes builds the arm64 library under themobileprofile, validates the example's Gradle wrapper, assembles the release AAR and checks it against a size baseline.rustls-webpki0.103.16 andwebpki-roots1.0.10 (RUSTSEC-2026-0381 / 0382) inCargo.lockandtools/capi-fuzz/Cargo.lock, with the three notices files.Not in this PR: Nostr, Reticulum, the telemetry pipe, the mesh foreground service, a pub.dev release, and a CI job that compiles the iOS plugin.
docs/bridges/flutter.mdhas the manual iOS typecheck until that job exists.Test plan
cargo test -p offline-protocol-uniffi --lib: 277 pass, including the seven Flutter guards. Each new guard was mutation-checked (stale exclude, excluded shared source, include list restored, podspec and rsync lists disagreeing, version literal restored) and fails.cd bindings/flutter/offline_protocol && flutter test && flutter analyze: 10 pass, no issues.Flutter.xcframeworkand the shared sources at iOS 13.0: no errors. The 12 warnings under the Xcode 26.5 SDK are all in shared React Native sources, none in the plugin's own classes; a deliberately broken copy fails.--profile mobile,:offline_protocol:assembleRelease, size baseline re-measured (5,447,562 bytes; the old baseline measured the--releaseprofile).cargo fmt --all -- --check,cargo clippy --workspace -- -D warnings.