NFC for Mob apps — read and write NDEF tags, read raw tag UIDs, and emulate a tag (Android HCE). iOS CoreNFC (iPhone 7+) / Android NfcAdapter + HostApduService.

CapabilityAPIiOSAndroid
Read NDEFstart_reading/2
Read raw tag (UID/type)start_reading(mode: :tag)
Write NDEFwrite_ndef/3
Emulate a tag (HCE)emulate_ndef/3— (unsupported)✅ read + writable

All paths are device-verified (iPhone SE 3rd gen ↔ Moto G power 5G 2024).

Install

# mix.exs
{:mob_nfc, github: "GenericJam/mob_nfc"}
# mob.exs — plugins are opt-in (a bare deps.get does nothing)
config :mob, :plugins, [:mob_nfc]

Use

Calls return the socket unchanged; results arrive in handle_info/2 as {:nfc, ...} messages to the process that started the session.

# Read
def handle_event("scan", _p, socket), do: {:noreply, MobNfc.start_reading(socket)}

def handle_info({:nfc, :ndef, %{ndef: bytes}}, socket) do
  records = MobNfc.Ndef.parse(bytes)   # one tested parser, shared across platforms
  uris = for r <- records, {:ok, u} <- [MobNfc.Ndef.decode_uri(r)], do: u
  {:noreply, assign(socket, :uris, uris)}
end

# Write
MobNfc.write_ndef(socket, MobNfc.Ndef.uri_record("https://mob.dev"))
MobNfc.write_ndef(socket, [MobNfc.Ndef.text_record("hi"), uri_rec])

# Emulate a tag (Android)
MobNfc.emulate_ndef(socket, MobNfc.Ndef.uri_record("https://mob.dev"))
MobNfc.emulate_ndef(socket, rec, writable: true)   # a reader can write into it

Events (tagged :nfc)

{:nfc, :session_started}
{:nfc, :ndef, %{tag_id: binary, ndef: binary, writable: boolean, max_size: integer}}
{:nfc, :tag, %{tag_id: binary, tech: binary}}      # a tag with no NDEF data
{:nfc, :written, %{bytes: integer}}                # write_ndef/3 succeeded
{:nfc, :emulation_started | :emulation_stopped}    # HCE lifecycle
{:nfc, :hce_read}                                  # a reader read the emulated tag
{:nfc, :hce_written, %{ndef: binary}}              # a reader wrote to it (writable HCE)
{:nfc, :session_ended, reason}                     # :done | :user_cancel | :timeout | ...
{:nfc, :error, reason}                             # :unavailable | :read_only | :too_small | ...

ndef is the raw NDEF message bytes — turn it into records with MobNfc.Ndef.parse/1 and decode Text/URI with decode_text/1 / decode_uri/1. MobNfc.available?/0 reports whether the radio is present and enabled; reading is a no-op on the simulator/emulator.

Host setup

The plugin adds what it can automatically (Android NFC permission, iOS NFCReaderUsageDescription). A few things mob_dev can't contribute yet must be added by hand — the native build prints them as host_requirements:

  • iOS NFC entitlement in ios/<app>.entitlements (+ mix mob.provision):
    <key>com.apple.developer.nfc.readersession.formats</key>
    <array><string>NDEF</string><string>TAG</string></array>
  • iOS raw-tag mode (mode: :tag) needs an ISO7816 select-identifiers AID list in ios/<app>.plist (an array plist key mob_dev can't merge yet). iOS blocks EMV payment cards regardless (Secure Element).
  • Android HCE (emulate_ndef/3) needs a <service> + res/xml/…apduservice.xml in the app (mob_dev can't contribute those yet).
  • Android — add <uses-feature android:name="android.hardware.nfc" android:required="false"/> so NFC-less devices still install.

See the plugin manifest host_requirements for the exact XML snippets.

Design notes

  • One NDEF parser/encoder (MobNfc.Ndef), in Elixir, shared by both platforms — the native layer only ships raw bytes.
  • MobNfc.Hce is a pure, tested reference implementation of the Type-4 Tag APDU state machine; the Android HostApduService mirrors it (an HCE service must answer readers even when the BEAM is down, so it can't delegate at runtime).
  • 53 host tests cover the wire format, the API transport, and the HCE protocol; the native CoreNFC / HostApduService glue is verified on-device.