
“The bridge is the whole job.”
Passport chips and payment terminals, from React Native
Two engagements, two kinds of hardware, one approach: write the protocol natively, expose it once, and let the JavaScript side stay ignorant of both.
Twice we had to make React Native talk to hardware that had no library worth using: the NFC chip in a German passport, and a Chinese payment terminal running Android.
Both times the answer had the same shape — implement the protocol in the platform’s own language, expose a narrow surface to JavaScript, and keep the platform detail from leaking upward. What differs is what the hardware demanded.
The problem
The constraint
What we did
The chip protocol in Java, the same in Swift
Implemented for Android, then ported to iOS and exposed to React Native through a Turbo Module — then still an experimental part of the new architecture, requiring manual registration and with thin documentation.
One protocol, two platformsThe port matters more than the implementation
Two independent implementations of a protocol like this drift: they start treating edge cases differently, and then the same passport passes on one phone and fails on another. In identity verification that is not a cosmetic inconsistency — it is unequal treatment of the person holding the document.
Same passport, same outcomeThe terminal exposed, then locked down
Card, printer and hardware exposed through a Turbo Module in Java. Then a launcher in Kotlin that replaced the Android home screen and locked the device to a single app. On a device that dispenses cash that is a control decision rather than a cosmetic one: the person behind the counter must not be able to open a browser, install anything, or change a setting.
One app, no way outIt paid for itself twice — updates over the network
A device locked to one app you control can be updated remotely. Over a hundred terminals were updated without anyone visiting a site — the app checked object storage for a new version, and if the update failed nothing happened at all: it kept running and retried every five minutes until it succeeded. Every terminal was registered in an admin dashboard showing its activity from the moment it connected through to who issued which coupon.
100+ terminals, zero site visitsThermal printing: the printer knows only dots
A receipt printer does not know about images, fonts or layout — it takes dots. A logo has to become a bitmap and be positioned by hand, in paper units, with no preview. It sounds trivial until you try it, and it can only be done by printing and measuring.
Print and measure
ICAO 9303, with an authentication handshake before the chip will say anything. Implemented in Java, ported to Swift, exposed through a Turbo Module. Decoding the retrieved data was an external service — ours was reading it off the chip.
The result
- Cryptographically signed passport data read from the chip, on both platforms, from one protocol implementation
- Over 100 payment terminals locked to a single app and updated over the network
- Failed updates costing nothing: the terminal kept working and retried
What we’d do differently
On the passport module we ported the Java implementation to Swift by hand and verified it by testing. What we should have built was a shared set of protocol test vectors — known inputs and expected outputs — run against both implementations. Testing that they behave the same is not the same as testing that they are correct, and we did the first while believing we had done the second.
- Java
- Kotlin
- Swift
- Turbo Modules
- React Native
- Android
- NFC / ICAO 9303