Skip to content

fix(bindings): forwardMessage's priority crosses the iOS bridge as a required int - #418

Merged
bahdotsh merged 1 commit into
mainfrom
fix/417-forward-message-priority-abi
Aug 26, 2026
Merged

fix(bindings): forwardMessage's priority crosses the iOS bridge as a required int#418
bahdotsh merged 1 commit into
mainfrom
fix/417-forward-message-priority-abi

Conversation

@bahdotsh

Copy link
Copy Markdown
Member

Closes #417.

The problem

React Native forces every NSNumber argument to non-null, because numbers are not nullable on Android, and refuses a null one before the Swift method is entered. Neither the resolver nor the rejecter runs, so the promise never settles and await forwardMessage(...) hangs forever behind a redbox.

The TypeScript passed null whenever a caller omitted the priority, which made forwardMessage the one method in this bridge relying on a nullable number. As #417 documents, no spelling of the declaration fixes it: nullable is rejected at module load, unspecified and nonnull are both forced non-null, and a plain NSInteger did not match the Swift NSNumber?.

The fix

The nullability is removed rather than respelled, because the null carried no information in the first place:

  • the core already does priority.unwrap_or(MessagePriority::Medium) (crates/offline-protocol/src/protocol/send.rs:259),
  • ForwardMessageParams.priority already documented "defaults to Medium", and
  • sendMessage already had the identical API and already resolves the default in TypeScript, crossing as a required int.

forwardMessage now matches its sibling exactly: TypeScript sends params.priority ?? MessagePriority.Medium, the shim takes NSInteger, Swift and Kotlin take Int, and each maps an unrecognised value back to Medium.

This is the RN bridge, not UniFFI output, so the UDL is untouched and no bindings were regenerated.

Impact

No caller sees a behaviour change on either platform: an omitted priority meant Medium before and means Medium now. The check that refused the null is inside #if RCT_DEBUG, so only development was affected. The public ForwardMessageParams type is unchanged.

Why a guard cannot hold this

react_native_ios_objc_shim_and_swift_agree_on_every_selector compares ABI classes, and a nullable number and a nullable object share one. Both halves agreed while React Native rejected the call anyway. So the rule is written down instead: both BRIDGE_MAINTENANCE.md and docs/bridges/swift.md lose the paragraph describing the breakage and gain the rule that a nullable number never crosses this bridge.

The TypeScript half is pinned by a new harness file, js-ci-harness/forward-priority.test.js, wired into npm run test:js. It covers the argument always being a number and the MessagePriority.Low-is-0 trap, where resolving the default with || instead of ?? would silently upgrade every Low forward to Medium.

Verification

Check Result
cargo test --workspace --lib pass
cargo clippy --workspace -- -D warnings pass
iOS ABI/selector guards (5 tests) pass
swiftc -typecheck, full hand-written iOS source set pass, negative control confirmed at the changed method
Android :offlineprotocol:testDebugUnitTest (clean-dir copy) BUILD SUCCESSFUL
npm run test:js (all 6 harness files) pass
New harness cases mutation-tested ?? null and || each fail the case that covers them

…required int

React Native forces every `NSNumber` argument to non-null, because numbers are
not nullable on Android, and refuses a null one before the Swift method is
entered: neither the resolver nor the rejecter runs, so the promise never
settles and `await forwardMessage(...)` hangs forever behind a redbox. The
TypeScript passed `null` whenever a caller omitted the priority, which made
`forwardMessage` the one method in the bridge relying on a nullable number.

No spelling of the declaration repairs that, so the nullability is gone
instead. The null carried no information in the first place: the core already
resolves an absent priority to Medium, the TypeScript already documented that
default, and `sendMessage` already resolves it in TypeScript and crosses as a
required int. `forwardMessage` now does the same, so the shim takes
`NSInteger`, Swift and Kotlin take `Int`, and each maps an unrecognised value
back to Medium.

No caller sees a behaviour change on either platform. The check that refused
the null is compiled out of release builds, so only development was affected.

The selector guard cannot catch a regression here, since a nullable number and
a nullable object share an ABI class and the two halves therefore agree. Both
bridge documents lose the paragraph describing the breakage and gain the rule,
and a new JS harness case pins the TypeScript half: the argument is always a
number, and `MessagePriority.Low` being 0 means the default has to be resolved
with `??` rather than `||`.

Closes #417
@bahdotsh
bahdotsh merged commit 028d60e into main Aug 26, 2026
18 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 26, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

forwardMessage's optional priority cannot cross the iOS bridge: the promise never settles in debug builds

1 participant