July 30, 2026 — The unofficial Laracon US Day 3. Get your ticket to The Vibes
Plugin Marketplace

weswecan/nfc

NFC plugin for NativePHP Mobile — read and write NFC tags on iOS and Android

NFC Plugin for NativePHP Mobile#

A full-featured NFC plugin for NativePHP Mobile. Read and write NFC tags on both iOS and Android with support for URLs, plain text, vCard contacts, JSON/MIME data, Android Application Records, tag erasure, and continuous scanning. Includes tag hardware info, platform-aware session options, typed events, and comprehensive error handling.

License: proprietary — see License and LICENSE.md before use.

Installation#

Copied!
composer require weswecan/nfc
 
php artisan native:plugin:register weswecan/nfc

JavaScript Setup#

Add the #nfc import mapping to your project's package.json:

Copied!
{
"imports": {
"#nfc": {
"types": "./vendor/weswecan/nfc/resources/js/dist/index.d.ts",
"default": "./vendor/weswecan/nfc/resources/js/dist/index.js"
}
}
}

Quick Start#

JavaScript (Vue/React/Inertia)#

Copied!
import { nfc, NfcEvent, NfcErrorCode, NfcTagType } from '#nfc';
 
// Check if NFC is available
const status = await nfc.isAvailable();
 
if (status.available && status.enabled) {
// Write URL to NFC tag
await nfc.writeUrl('https://example.com');
 
// Or read from NFC tag
await nfc.read();
} else if (status.available && !status.enabled) {
// Open settings to enable NFC (Android only — see platform notes)
await nfc.openSettings();
}
 
// Listen for successful write
nfc.on(NfcEvent.UrlWritten, (data) => {
console.log('URL written:', data.url);
});
 
// Listen for tag read with tag info
nfc.on(NfcEvent.TagRead, (data) => {
console.log('Content:', data.content);
console.log('Type:', data.type);
if (data.tagInfo) {
console.log('Capacity:', data.tagInfo.capacity, 'bytes');
console.log('Writable:', data.tagInfo.isWritable);
}
});
 
// Listen for errors
nfc.on(NfcEvent.Error, async (data) => {
if (data.code === NfcErrorCode.NfcDisabled) {
await nfc.openSettings(); // Android only
}
});

PHP (Livewire/Blade)#

Copied!
use Nativephp\Nfc\Facades\Nfc;
 
$status = Nfc::isAvailable();
 
if ($status->available && $status->enabled) {
Nfc::writeUrl('https://example.com');
}

Platform Support Matrix#

Feature iOS Android Notes
isAvailable() See platform notes below
read()
writeUrl()
writeText()
writeRecord() - text
writeRecord() - uri
writeRecord() - vcard iOS can write but not read vCards — see below
writeRecord() - mime
writeRecord() - aar Android Application Record
writeUrlWithAar() ✅* *iOS writes URL only, ignores AAR
erase() Writes empty NDEF message
cancel()
openSettings() ⚠️ iOS: opens Settings app but NFC cannot be toggled — not useful
isIos() / isAndroid() Platform detection
Tag Info Capacity, writable, technologies
Tag ID Hardware UID — iOS NDEF sessions do not expose tag identifiers

Session Options by Platform#

Option iOS Android Description
timeout Session timeout in seconds (iOS manages its own timeout)
alertMessage Custom message shown in NFC prompt
successMessage Message shown after successful operation
keepSessionActive Keep session active for continuous scanning

Platform-Specific Notes#

iOS isAvailable() Behavior

On iOS, NFC cannot be disabled by the user — it is always enabled on supported devices (iPhone 7+). As a result, isAvailable() returns the same value for both available and enabled:

  • Supported device: { available: true, enabled: true }
  • Unsupported device or simulator: { available: false, enabled: false }

The available: true, enabled: false state only occurs on Android where users can toggle NFC in settings.

vCard Reading on iOS

Limitation: iOS can write vCards to NFC tags but cannot correctly read them back. When iOS reads a tag containing a native vCard MIME type (text/vcard), it returns the raw content with a MIME type string instead of the vcard type. This is a platform limitation of iOS's CoreNFC framework. Android handles both reading and writing vCards correctly.

NfcCancelled Event on Android

On Android, the NfcCancelled event is not dispatched when cancel() is called. This is because Android's Reader Mode API has no user-facing NFC dialog that can be cancelled (unlike iOS's NFC sheet). The cancel() call returns success synchronously, and your app should update UI state from the promise response. On iOS, both programmatic cancellation and user-initiated cancellation (dismissing the NFC sheet) dispatch the NfcCancelled event.

API Reference#

Platform Detection#

Detect the current platform to conditionally enable features.

Copied!
// Check platform
const platform = nfc.getPlatform();
// { isIos: true, isAndroid: false, isMobile: true }
 
// Convenience methods
if (nfc.isAndroid()) {
// Use timeout option
}
 
if (nfc.isIos()) {
// Use alertMessage option
}

isAvailable()#

Check if NFC is available and enabled on the device.

Copied!
const status = await nfc.isAvailable();
// { available: true, enabled: true, status: 'enabled', message: '...' }

writeUrl(url, options?)#

Write a URL to an NFC tag.

Copied!
// Basic usage
await nfc.writeUrl('https://example.com');
 
// With session options (platform-specific)
await nfc.writeUrl('https://example.com', {
timeout: 15, // Android only
alertMessage: 'Tap the NFC tag', // iOS only
successMessage: 'URL saved!' // iOS only
});

writeText(text, languageCode?, options?)#

Write plain text to an NFC tag.

Copied!
// Basic usage
await nfc.writeText('Hello World');
 
// With language code
await nfc.writeText('Bonjour le monde', 'fr');
 
// With session options
await nfc.writeText('Hello', 'en', { timeout: 20 });

writeRecord(record, options?)#

Write various NDEF record types to an NFC tag.

Copied!
// Write a URI
await nfc.writeRecord({ type: 'uri', uri: 'https://example.com' });
 
// Write text with language
await nfc.writeRecord({ type: 'text', text: 'Hello', languageCode: 'en' });
 
// Write a vCard contact
await nfc.writeRecord({
type: 'vcard',
vcard: {
firstName: 'John',
lastName: 'Doe',
phone: '+1234567890',
organization: 'Acme Corp',
title: 'Developer',
url: 'https://example.com'
}
});
 
// Write JSON data via MIME
await nfc.writeRecord({
type: 'mime',
mimeType: 'application/json',
data: JSON.stringify({ userId: 123, action: 'checkin' })
});
 
// Write Android Application Record (Android only)
await nfc.writeRecord({ type: 'aar', packageName: 'com.example.myapp' });

writeUrlWithAar(url, packageName, options?)#

Write a URL and Android Application Record (AAR) to a single NFC tag. When scanned on Android, this opens the URL in the browser or launches the specified app if installed. On iOS, only the URL is written (AAR is ignored).

Copied!
// Write URL + AAR combo (standard Android NFC deployment pattern)
await nfc.writeUrlWithAar('https://example.com', 'com.example.myapp');
 
// With session options
await nfc.writeUrlWithAar('https://example.com', 'com.example.myapp', {
timeout: 15,
alertMessage: 'Tap the NFC tag'
});

erase(options?)#

Erase an NFC tag by writing an empty NDEF message.

Copied!
// Erase a tag
await nfc.erase();
 
// With session options
await nfc.erase({ alertMessage: 'Tap the tag to erase' });
 
// Listen for success
nfc.on(NfcEvent.TagErased, (data) => {
console.log('Tag erased!', data.tagId);
});

read(options?)#

Read data from an NFC tag. Returns tag content and hardware information.

Copied!
// Basic usage
await nfc.read();
 
// With session options
await nfc.read({
timeout: 30,
alertMessage: 'Hold near product tag',
keepSessionActive: true // Read multiple tags in one session
});
 
// Handle the result
nfc.on(NfcEvent.TagRead, (data) => {
console.log('Content:', data.content);
console.log('Type:', data.type); // url, text, vcard, empty, aar, mime type string, etc.
console.log('Tag ID:', data.tagId); // Android only — empty string on iOS
 
// Tag hardware information
if (data.tagInfo) {
console.log('Capacity:', data.tagInfo.capacity, 'bytes');
console.log('Used:', data.tagInfo.usedSize, 'bytes');
console.log('Writable:', data.tagInfo.isWritable);
console.log('Technologies:', data.tagInfo.technologies);
console.log('Can lock:', data.tagInfo.canMakeReadOnly);
}
});

cancel()#

Cancel the current NFC session.

Copied!
await nfc.cancel();

openSettings()#

Open the device's NFC settings. Only useful on Android where users can toggle NFC on/off. On iOS, NFC cannot be disabled by the user, so this function has no practical effect (it opens the general Settings app).

Copied!
// Android only — check if NFC is disabled and prompt the user
const status = await nfc.isAvailable();
if (nfc.isAndroid() && status.available && !status.enabled) {
await nfc.openSettings();
}

writeWifiConfig(ssid, password, security?, options?)#

Write WiFi credentials to an NFC tag. Uses the standard application/vnd.wfa.wsc MIME type. When an Android device scans the tag, it offers to connect to the network automatically. Useful for hotels, offices, cafes, and events.

Copied!
// Write WiFi config (WPA2 by default)
await nfc.writeWifiConfig('MyNetwork', 'MyPassword');
 
// Specify security type
await nfc.writeWifiConfig('MyNetwork', 'MyPassword', 'WPA2');
 
// Open network (no password)
await nfc.writeWifiConfig('GuestWifi', '', 'OPEN');
 
// Supported security types: 'WPA2', 'WPA', 'WEP', 'OPEN'

Session Options#

Configure NFC session behavior with optional parameters:

Copied!
interface NfcSessionOptions {
/** Timeout in seconds (default: 30, set to 0 for no timeout) */
timeout?: number;
 
/** Custom message shown during NFC prompt */
alertMessage?: string;
 
/** Message after success (iOS only) */
successMessage?: string;
 
/** Keep session active after first tag (iOS and Android) */
keepSessionActive?: boolean;
}

Unsupported options are logged to console and gracefully ignored.

Continuous Scanning#

Use keepSessionActive: true to read multiple tags in one session. On iOS, the NFC sheet stays open. On Android, Reader Mode remains active.

Copied!
await nfc.read({ keepSessionActive: true });
 
// Each tag fires a separate TagRead event
nfc.on(NfcEvent.TagRead, (data) => {
console.log('Scanned:', data.content);
});
 
// Call cancel() when done
await nfc.cancel();

Events#

NfcUrlWritten#

Dispatched when a URL is successfully written.

  • url - The URL that was written
  • tagId - Tag identifier (Android only, empty string on iOS)

NfcTextWritten#

Dispatched when text is successfully written.

  • text - The text that was written
  • languageCode - Language code used
  • tagId - Tag identifier (Android only, empty string on iOS)

NfcRecordWritten#

Dispatched when a custom record is successfully written.

  • recordType - Type of record (text, uri, vcard, mime, aar)
  • tagId - Tag identifier (Android only, empty string on iOS)

NfcTagRead#

Dispatched when a tag is successfully read.

  • content - Content read from the tag
  • type - Content type: url, text, vcard, empty, aar, mifare_classic, mifare_ultralight, unknown, or a MIME type string (e.g. application/json)
  • tagId - Tag identifier (Android only, empty string on iOS)
  • tagInfo - Tag hardware information (capacity, usedSize, isWritable, technologies, canMakeReadOnly)

NfcTagErased#

Dispatched when a tag is successfully erased.

  • tagId - Tag identifier (Android only, empty string on iOS)

NfcError#

Dispatched when an error occurs.

  • code - Error code
  • message - Human-readable error message

NfcCancelled#

Dispatched when the session is cancelled (iOS only — see platform notes above).

  • reason - Reason for cancellation

Error Codes#

Code Description Action
TIMEOUT Session timed out Retry or extend timeout
NO_TAG No tag detected Ensure tag is present
READ_ERROR Error reading tag Tag may have been removed
WRITE_ERROR Error writing tag Check tag capacity
WRITE_FAILED Write failed Tag may be read-only
INVALID_URL Invalid URL format Check URL scheme
INVALID_PARAMETERS Missing parameters Check function arguments
INVALID_RECORD_TYPE Invalid record type Use text, uri, vcard, mime, or aar
NFC_NOT_SUPPORTED No NFC hardware Device doesn't support NFC
NFC_DISABLED NFC disabled Call openSettings()
SESSION_START_FAILED Session failed Try again
UNSUPPORTED_PLATFORM Feature not on platform AAR not on iOS
NOT_SUPPORTED Tag doesn't support NDEF Use an NDEF-compatible tag
READ_ONLY Tag is read-only Tag has been locked
PAYLOAD_ERROR Failed to create NDEF payload Check record data
MESSAGE_TOO_LARGE Content exceeds tag capacity Reduce content or use larger tag
CONNECTION_FAILED Failed to connect to tag (iOS) Tag may have been moved
QUERY_FAILED Failed to query tag status (iOS) Retry the operation
SYSTEM_BUSY NFC system is busy (iOS) Wait and try again

Framework Examples#

Vue (Inertia)#

A complete NFC scanner component using Vue 3 with Composition API.

Copied!
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
import { nfc, NfcEvent, NfcErrorCode, type NfcTagReadPayload } from '#nfc';
 
const isAvailable = ref(false);
const isEnabled = ref(false);
const isScanning = ref(false);
const result = ref<NfcTagReadPayload | null>(null);
const error = ref<string | null>(null);
 
onMounted(async () => {
const status = await nfc.isAvailable();
isAvailable.value = status.available;
isEnabled.value = status.enabled;
});
 
const cleanupRead = nfc.on(NfcEvent.TagRead, (data) => {
result.value = data;
isScanning.value = false;
});
 
const cleanupError = nfc.on(NfcEvent.Error, (data) => {
error.value = `${data.code}: ${data.message}`;
isScanning.value = false;
 
if (data.code === NfcErrorCode.NfcDisabled && nfc.isAndroid()) {
nfc.openSettings();
}
});
 
const cleanupCancel = nfc.on(NfcEvent.Cancelled, () => {
isScanning.value = false;
});
 
onUnmounted(() => {
cleanupRead();
cleanupError();
cleanupCancel();
});
 
async function scan() {
error.value = null;
result.value = null;
isScanning.value = true;
await nfc.read({ alertMessage: 'Hold near NFC tag' });
}
 
async function writeUrl(url: string) {
error.value = null;
isScanning.value = true;
await nfc.writeUrl(url, { alertMessage: 'Tap tag to write URL' });
}
</script>
 
<template>
<div v-if="!isAvailable">
<p>NFC is not supported on this device.</p>
</div>
<div v-else-if="!isEnabled">
<p>NFC is disabled.</p>
<button @click="nfc.openSettings()">Open NFC Settings</button>
</div>
<div v-else>
<button @click="scan" :disabled="isScanning">
{{ isScanning ? 'Scanning...' : 'Scan NFC Tag' }}
</button>
<button @click="writeUrl('https://example.com')" :disabled="isScanning">
Write URL
</button>
 
<div v-if="result">
<p>Content: {{ result.content }}</p>
<p>Type: {{ result.type }}</p>
<p v-if="result.tagInfo">
Capacity: {{ result.tagInfo.capacity }} bytes
({{ result.tagInfo.isWritable ? 'writable' : 'read-only' }})
</p>
</div>
 
<p v-if="error" style="color: red;">{{ error }}</p>
</div>
</template>

React (Inertia)#

The same NFC scanner as a React functional component.

Copied!
import { useState, useEffect, useCallback } from 'react';
import { nfc, NfcEvent, NfcErrorCode, type NfcTagReadPayload } from '#nfc';
 
export default function NfcScanner() {
const [isAvailable, setIsAvailable] = useState(false);
const [isEnabled, setIsEnabled] = useState(false);
const [isScanning, setIsScanning] = useState(false);
const [result, setResult] = useState<NfcTagReadPayload | null>(null);
const [error, setError] = useState<string | null>(null);
 
useEffect(() => {
nfc.isAvailable().then((status) => {
setIsAvailable(status.available);
setIsEnabled(status.enabled);
});
 
const cleanupRead = nfc.on(NfcEvent.TagRead, (data) => {
setResult(data);
setIsScanning(false);
});
 
const cleanupError = nfc.on(NfcEvent.Error, (data) => {
setError(`${data.code}: ${data.message}`);
setIsScanning(false);
 
if (data.code === NfcErrorCode.NfcDisabled && nfc.isAndroid()) {
nfc.openSettings();
}
});
 
const cleanupCancel = nfc.on(NfcEvent.Cancelled, () => {
setIsScanning(false);
});
 
return () => {
cleanupRead();
cleanupError();
cleanupCancel();
};
}, []);
 
const scan = useCallback(async () => {
setError(null);
setResult(null);
setIsScanning(true);
await nfc.read({ alertMessage: 'Hold near NFC tag' });
}, []);
 
const writeUrl = useCallback(async (url: string) => {
setError(null);
setIsScanning(true);
await nfc.writeUrl(url, { alertMessage: 'Tap tag to write URL' });
}, []);
 
if (!isAvailable) {
return <p>NFC is not supported on this device.</p>;
}
 
if (!isEnabled) {
return (
<div>
<p>NFC is disabled.</p>
<button onClick={() => nfc.openSettings()}>Open NFC Settings</button>
</div>
);
}
 
return (
<div>
<button onClick={scan} disabled={isScanning}>
{isScanning ? 'Scanning...' : 'Scan NFC Tag'}
</button>
<button onClick={() => writeUrl('https://example.com')} disabled={isScanning}>
Write URL
</button>
 
{result && (
<div>
<p>Content: {result.content}</p>
<p>Type: {result.type}</p>
{result.tagInfo && (
<p>
Capacity: {result.tagInfo.capacity} bytes
({result.tagInfo.isWritable ? 'writable' : 'read-only'})
</p>
)}
</div>
)}
 
{error && <p style={{ color: 'red' }}>{error}</p>}
</div>
);
}

Livewire#

A complete Livewire component with event handlers.

Component class:

Copied!
<?php
 
namespace App\Livewire;
 
use Livewire\Component;
use Native\Mobile\Attributes\OnNative;
use Nativephp\Nfc\Facades\Nfc;
use Nativephp\Nfc\Events\NfcTagRead;
use Nativephp\Nfc\Events\NfcUrlWritten;
use Nativephp\Nfc\Events\NfcTextWritten;
use Nativephp\Nfc\Events\NfcRecordWritten;
use Nativephp\Nfc\Events\NfcTagErased;
use Nativephp\Nfc\Events\NfcError;
use Nativephp\Nfc\Events\NfcCancelled;
 
class NfcScanner extends Component
{
public ?string $content = null;
public ?string $type = null;
public ?array $tagInfo = null;
public ?string $error = null;
public bool $isScanning = false;
 
public function scan(): void
{
$this->reset('content', 'type', 'tagInfo', 'error');
$this->isScanning = true;
Nfc::read(['alertMessage' => 'Hold near NFC tag']);
}
 
public function writeUrl(string $url): void
{
$this->reset('error');
$this->isScanning = true;
Nfc::writeUrl($url, ['alertMessage' => 'Tap tag to write URL']);
}
 
#[OnNative(NfcTagRead::class)]
public function handleTagRead(string $content, string $type, ?string $tagId = null, ?array $tagInfo = null): void
{
$this->content = $content;
$this->type = $type;
$this->tagInfo = $tagInfo;
$this->isScanning = false;
}
 
#[OnNative(NfcUrlWritten::class)]
public function handleUrlWritten(string $url, ?string $tagId = null): void
{
$this->content = $url;
$this->type = 'url';
$this->isScanning = false;
}
 
#[OnNative(NfcTextWritten::class)]
public function handleTextWritten(string $text, string $languageCode, ?string $tagId = null): void
{
$this->content = $text;
$this->isScanning = false;
}
 
#[OnNative(NfcRecordWritten::class)]
public function handleRecordWritten(string $recordType, ?string $tagId = null): void
{
$this->type = $recordType;
$this->isScanning = false;
}
 
#[OnNative(NfcTagErased::class)]
public function handleTagErased(?string $tagId = null): void
{
$this->content = null;
$this->type = null;
$this->isScanning = false;
}
 
#[OnNative(NfcError::class)]
public function handleNfcError(string $code, string $message): void
{
$this->error = "{$code}: {$message}";
$this->isScanning = false;
}
 
#[OnNative(NfcCancelled::class)]
public function handleNfcCancelled(string $reason = 'user_cancelled'): void
{
$this->isScanning = false;
}
 
public function render()
{
return view('livewire.nfc-scanner');
}
}

Blade template (resources/views/livewire/nfc-scanner.blade.php):

Copied!
<div>
<button wire:click="scan" @disabled($isScanning)>
{{ $isScanning ? 'Scanning...' : 'Scan NFC Tag' }}
</button>
<button wire:click="writeUrl('https://example.com')" @disabled($isScanning)>
Write URL
</button>
 
@if ($content)
<p>Content: {{ $content }}</p>
<p>Type: {{ $type }}</p>
@if ($tagInfo)
<p>
Capacity: {{ $tagInfo['capacity'] }} bytes
({{ $tagInfo['isWritable'] ? 'writable' : 'read-only' }})
</p>
@endif
@endif
 
@if ($error)
<p style="color: red;">{{ $error }}</p>
@endif
</div>

Features Not Included#

The following features are intentionally not supported:

Lock Tags#

Making tags permanently read-only is not included due to its irreversible nature. Once locked, a tag can never be written to again.

Raw Tag Commands#

Low-level tag communication (transceive, MIFARE Classic sector read/write, ISO-DEP APDUs) is not supported. This plugin operates at the NDEF level only.

Host Card Emulation (HCE)#

Making the device act as an NFC tag (Host Card Emulation) is not supported. This feature is Android-only and requires complex service registration.

Background Tag Detection#

Automatically handling NFC tags when the app is in the background is not implemented. This requires platform-specific configuration and varies significantly between iOS and Android.

Platform Requirements#

Android#

  • Android 7.0+ (SDK 24)
  • NFC hardware
  • NFC enabled in settings

iOS#

  • iPhone 7 or newer
  • iOS 13+
  • NFC entitlement in app

Support#

For technical issues or questions, email [email protected].

For licensing, seats, or purchase, use the same address — see License.

License#

Proprietary software (composer.json declares license: proprietary). Use is governed by the End User License Agreement (EULA) in LICENSE.md. By installing, copying, or using this package, you agree to that agreement.

The following is a summary only and does not replace the full EULA:

Topic Summary
Copyright © 2026–present Context Undefined. All rights reserved.
Seats One license per developer who accesses, modifies, or compiles the Software’s source; additional developers require additional seats.
You may Use on your projects and in client work when integrated into a final product (not as a standalone offering); modify for integration in authorized projects.
You may not Redistribute, sublicense, or sell the Software; publish source on public GitHub or registries; use it to build a competing product; remove proprietary notices; share license credentials.

Governing law and liability terms are in LICENSE.md (Netherlands).

Licensing inquiries: [email protected]