Reading the tag
Two entry points, because a tag that started the app and a tag that arrived while it was running are genuinely different situations.withTag
does. A native handle that outlives its scope is a leak whose failure surfaces
somewhere else entirely, so there is no version of these that hands one out and
trusts you to give it back.
Two more behaviours worth knowing:
withLaunchTagconsumes the tag: a second call answersnull. That is also what stops a screen rotation from replaying a tap from minutes ago, since the recreated activity is handed the same launch intent.tag.onLostnever fires for a background tag. It is not being watched, because a watcher would do nothing but announce a departure that happened before the app was looking.
Expo
app.json
NDEF filters
Each entry becomes one intent filter, matched against the first record of the tag’s message. Filter on a MIME type you control, or on your own URI scheme. They are kept as separate filters rather than merged, because Android combines sibling<data> attributes combinatorially: one filter holding both
scheme="https" and mimeType="text/plain" matches any https URI or any
text/plain payload, which is not what the config appears to say.
One caveat that catches people out: from Android 16, an NDEF tag holding an
http or https URI
dispatches ACTION_VIEW instead of ACTION_NDEF_DISCOVERED, so a scheme filter for those two no
longer fires on newer devices. Your own scheme and MIME-type filters are unaffected.Tech lists
Each inner array is one<tech-list>, and a tag matches when it supports every
technology in that list. Separate lists are alternatives. So the example above
matches an ISO-DEP tag, or a tag that is both MIFARE Ultralight and NDEF.
ACTION_TAG_DISCOVERED is never written. It is deprecated as of API 37, and it is
also the widest filter there is — it fires for any tag at all, including ones your
app has no idea what to do with.
iOS
There is nothing to configure. iOS reads NDEF tags in the background on its own, without the app being involved, and opens a URL record’s link. It cannot be extended to other technologies and cannot be turned off by an app.The Android 17 permission
From Android 17 (API 37), an activity receiving NFC intents must be protected byandroid.permission.DISPATCH_NFC_MESSAGE when the app targets an SDK above
BAKLAVA, so that only the NFC system service can dispatch to it.
The trap is that android:permission on an activity applies on every Android
version, and a permission the running platform does not define can be held by
nobody — so applying it on a device older than API 37 stands to block the very
dispatch it is meant to secure. One manifest ships to every version, so this is a
real choice, not a formality.
dispatchNfcMessagePermission therefore defaults to 'auto', which is narrow on
purpose:
To let
'auto' decide, declare the target SDK explicitly:
app.json
Bare React Native
android/app/src/main/AndroidManifest.xml
android/app/src/main/res/xml/nfc_kit_tech_filter.xml
<meta-data> above points at it — but keeping this name means a later prebuild
would overwrite the same file rather than leaving two.
iOS
Checking it works
1
The first record does not match the filter
Only the first record of the message is matched against an NDEF filter.
2
The activity is missing singleTop
Android recreates it on every intent otherwise.
3
You are on API 37 with the permission set the wrong way
See the section above;
dispatchNfcMessagePermission decides it outright.