Skip to content

fix(new-arch): align TurboModule openPicker signature across iOS, Android, and JS#967

Open
idanlevi1 wants to merge 2 commits into
henninghall:masterfrom
idanlevi1:fix/turbomodule-arg-count-mismatch
Open

fix(new-arch): align TurboModule openPicker signature across iOS, Android, and JS#967
idanlevi1 wants to merge 2 commits into
henninghall:masterfrom
idanlevi1:fix/turbomodule-arg-count-mismatch

Conversation

@idanlevi1
Copy link
Copy Markdown

@idanlevi1 idanlevi1 commented May 27, 2026

Problem

Under React Native's new architecture, the TurboModule runtime enforces strict argument-count parity between the codegen spec, the platform native module, and every JS caller. `openPicker` currently disagrees across five places:

Layer Args
iOS native (`ios/RNDatePickerManager.mm`) 3 (uses callbacks)
Android native newarch (`DatePickerModule`) 1
Codegen spec (`src/fabric/NativeRNDatePicker.ts`) 1
JS caller iOS (`src/modal.js`) 3
JS caller Android (`src/modal.js`) 1

So one of the two platforms is always wrong:

  • With the spec at 1 arg, iOS's 3-arg invocation is over by 2. RN currently permits this, but it is undocumented and fails strict tooling — and the iOS branch wouldn't work at all if RN tightened that check.
  • If a consumer patches the spec to 3 args to match iOS native (a common workaround once they hit `TurboModule method "openPicker" called with 1 arguments (expected argument count: 3)` — see e.g. Changing date/time color #883, downstream patches in the wild), Android JS starts crashing on every modal open because its caller still passes 1 arg.

Repro for the consumer case: RN 0.85 + new architecture, Android — open any modal date picker (`modal` mode in the component) → strict arg-count crash.

Fix

Make all five layers agree on 3 args:

  • `src/fabric/NativeRNDatePicker.ts` — spec now declares `openPicker(props, onConfirm, onCancel)`.
  • `android/src/newarch/java/.../DatePickerModule.java` — accepts `Callback onConfirm, Callback onCancel` and ignores them; Android continues to deliver confirm/cancel via the existing `RCTDeviceEventEmitter` flow (`DatePickerModuleImpl`).
  • `src/modal.js` — Android branch now passes two no-op callbacks, so the 3-arg signature is honoured on both platforms. iOS branch unchanged.

iOS behaviour is unchanged at runtime (native already takes 3 args and uses them). Android behaviour is unchanged at runtime (the callbacks are unused; events still flow via the emitter listeners registered in `useModal`).

Why this direction (3 args, not 1)

The alternative (move iOS off callbacks onto the emitter pattern so the spec could shrink to 1 arg) is the cleaner long-term refactor — and looks like the direction `upstream/unify-native-api` is heading. But that branch is still WIP and a much bigger change. The 3-arg alignment in this PR is the minimum diff that unblocks new-arch consumers today without breaking anything; the deeper unification can land on top later.

Test plan

  • Android new-arch + RN 0.85: `` opens and closes, confirm/cancel events still fire
  • Android old-arch: builds and runs as before
  • iOS new-arch: `` opens and closes, confirm/cancel still fire via callbacks
  • iOS old-arch: unchanged

Happy to add a test in `example` if desired.

…roid, and JS

Under React Native's new architecture, the TurboModule runtime enforces
strict argument-count parity between the JS-side spec, the platform
native module, and every JS caller. `react-native-date-picker`
currently has three sources of truth for `openPicker` and they
disagree:

| Layer                                         | Args          |
|-----------------------------------------------|---------------|
| iOS native (`ios/RNDatePickerManager.mm`)     | 3 (uses cbs)  |
| Android native newarch (`DatePickerModule`)   | 1             |
| Codegen spec (`src/fabric/NativeRNDatePicker.ts`) | 1          |
| JS caller iOS  (`src/modal.js`)               | 3             |
| JS caller Android (`src/modal.js`)            | 1             |

So one of the two platforms is always wrong:

- With the spec at 1 arg, the iOS caller's 3-arg invocation is over by
  two (RN currently permits this, but it is undocumented behaviour and
  fails strict tooling).
- If a consumer patches the spec to 3 args to match iOS (a common
  workaround once they hit `TurboModule method "openPicker" called with
  1 arguments (expected argument count: 3)`), the Android JS caller
  starts crashing on every modal open.

This PR makes all five layers agree on 3 args:

- `src/fabric/NativeRNDatePicker.ts` — spec now declares
  `openPicker(props, onConfirm, onCancel)`.
- `android/src/newarch/java/.../DatePickerModule.java` — accepts
  `Callback onConfirm, Callback onCancel` and ignores them; Android
  continues to deliver confirm/cancel via the existing
  `RCTDeviceEventEmitter` flow.
- `src/modal.js` — Android branch now passes two no-op callbacks so
  the 3-arg signature is honoured on both platforms.

iOS behaviour is unchanged at runtime (its native side already takes
3 args and uses them). Android behaviour is unchanged at runtime (the
callbacks are unused; events still flow via the emitter listeners
registered in `useModal`).

Encountered downstream in a React Native 0.85 + new-arch banking app
where opening any modal date picker (transaction filter date range,
loan payment day) on Android crashed with the strict arg-count error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@idanlevi1 idanlevi1 force-pushed the fix/turbomodule-arg-count-mismatch branch from 9337c44 to 574a2f9 Compare May 27, 2026 07:16
@idanlevi1
Copy link
Copy Markdown
Author

Pushed lint fix (yarn lint --fix). Two pre-existing prettier issues in src/DatePickerIOS.js:26-27 were auto-corrected — unrelated to this PR but were blocking the lint job.

The iOS build job is failing on a CI-infrastructure issue, not a code one: the runner uses Ruby 2.7.5 but the current CocoaPods install requires ffi >= 1.15.0 which needs Ruby >= 3.0 (The last version of ffi (>= 1.15.0) to support your Ruby & RubyGems was 1.17.4. Try installing it with gem install ffi -v 1.17.4). I can't fix that from a PR — happy to also pin the Ruby toolchain in the workflow if you'd like, otherwise this just needs the Actions runner config bumped to Ruby 3.x.

`pr.yml` is triggered by `pull_request_target`, which by default runs in
the base-branch (master) context. Most workflows already override the
checkout ref to the PR head:

  - uses: actions/checkout@v4
    with:
      ref: ${{ github.event.pull_request.head.sha }}

`lint.yml` was missing that override, so lint was always run against
master's source rather than the PR head — meaning lint failures on the
PR could not be fixed by the PR, and prettier issues already in master
permanently broke every contributor's lint check.

Mirror the existing pattern from build-android.yml / build-ios.yml /
test-android-*.yml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@idanlevi1
Copy link
Copy Markdown
Author

Lint failures are a CI config bug, not a code issue:

pr.yml is triggered by pull_request_target, which checks out the base branch (master) by default. build-android.yml, build-ios.yml, test-android-*.yml all override the checkout ref to the PR head:

- uses: actions/checkout@v4
  with:
    ref: ${{ github.event.pull_request.head.sha }}

…but lint.yml was missing that override, so lint was always running against master, not against the PR head. The errors it kept reporting (src/DatePickerIOS.js:26-27 prettier) are pre-existing in master and not part of this PR — but they kept failing the PR check because the check never saw the PR's fix.

Pushed an additional commit mirroring the existing pattern from the other workflows. Same fix probably applies to typecheck.yml (it currently passes only because master happens to typecheck clean — any contributor's PR-level type fix would be invisible to that job).

For the iOS Build job, that's a separate runtime/CI issue (Ruby 2.7.5 on the macOS runner can't install the latest CocoaPods because ffi requires Ruby >= 3.0). Same comment as before — that needs the Ruby toolchain on the runner bumped.

@idanlevi1
Copy link
Copy Markdown
Author

Confirming: my CI fixes can't pass these checks from a PR. pr.yml uses pull_request_target which always loads the workflow files from master, not from the PR head — so my workflow patch to lint.yml (adding ref: github.event.pull_request.head.sha) only takes effect once it's already merged. Until then the lint job will keep checking out master and reporting master's pre-existing prettier issues in DatePickerIOS.js:26-27.

So this needs a maintainer action — either:

  1. push the 2-line prettier fix to master directly (run yarn lint --fix on master), or
  2. push the lint.yml workflow fix to master directly, or
  3. waive / re-run the lint check on this PR after reviewing the actual code diff (which only touches src/modal.js, src/fabric/NativeRNDatePicker.ts, android/.../DatePickerModule.java, plus the lint.yml fix).

Happy to split the CI-config fix out into a separate PR against master so it lands cleanly first, then this one can be re-run on the fixed CI — but I'll stop pushing further commits to this branch until you've decided which path you'd prefer.

iOS Build is still the separate Ruby/CocoaPods issue (ffi >= 1.15.0 needs Ruby ≥ 3.0; runner is on 2.7.5).

@idanlevi1
Copy link
Copy Markdown
Author

Opened a small parallel PR (#968) with just the two CI-hygiene fixes needed to unblock the lint check on this repo — auto-fixing the pre-existing prettier issues in master's DatePickerIOS.js and adding the ref: github.event.pull_request.head.sha override that lint.yml was missing. Once #968 merges to master, this PR's lint check should pass on the next re-run.

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.

1 participant