Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Custom SwiftLint configuration for the plugin sources
# Exclude dependency and build directories to avoid third-party violations
excluded:
- node_modules
- .build
- dist
- android

# Limit linting to our Swift sources
included:
- ios/Sources/NFCPlugin

# Allow slightly longer lines for debug logging
line_length:
warning: 160
error: 200

# Opt-in / opt-out adjustments
opt_in_rules:
- unused_import

disabled_rules:
- force_cast # Acceptable in limited bridging contexts
- cyclomatic_complexity # Keep simple for now; plugin code is small

# Analyzer rules (run with `swiftlint analyze`) kept minimal
analyzer_rules:
- unused_import
103 changes: 93 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,25 @@ In Xcode:
5. Click the `+ Capability` button.
6. Add **Near Field Communication Tag Reading**.

> **Advanced tag formats:** If you need ISO 7816, ISO 15693, or FeliCa access (to read raw UIDs, system codes, etc.), Apple requires additional entitlements in your provisioning profile and `Info.plist`. The plugin will fall back automatically when they are absent, but to unlock the full feature set add the relevant keys:
>
> ```xml
> <key>com.apple.developer.nfc.readersession.felica.systemcodes</key>
> <array>
> <string>12FC</string>
> <string>0000</string>
> </array>
> <key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
> <array>
> <string>D2760000850100</string>
> <string>D2760000850101</string>
> <string>D2760001180101</string>
> <string>00000000000000</string>
> </array>
> ```
>
> Replace the sample identifiers with the values required for your tags. Consult Apple's CoreNFC documentation for the complete list of entitlement keys.

### 2. Add Usage Description

Add the `NFCReaderUsageDescription` key to your `Info.plist` file to explain why your app needs access to NFC.
Expand Down Expand Up @@ -116,14 +135,17 @@ NFC.onRead((data: NDEFMessagesTransformable) => {
console.log('First record raw bytes length:', asUint8.messages[0]?.records[0]?.payload.length);

// Access tag information (UID, tech types, etc.)
if (asString.tagInfo) {
console.log('Tag UID:', asString.tagInfo.uid);
console.log('Tag technologies:', asString.tagInfo.techTypes);
console.log('Tag type:', asString.tagInfo.type);
if (asString.tagInfo.maxSize) {
console.log('Max NDEF size:', asString.tagInfo.maxSize);
const info = asString.tagInfo;
if (info?.fallback) {
console.log('Reader fallback mode:', info.fallbackMode, 'Reason:', info.reason);
} else if (info) {
console.log('Tag UID:', info.uid);
console.log('Tag technologies:', info.techTypes);
console.log('Tag type:', info.type);
if (info.maxSize) {
console.log('Max NDEF size:', info.maxSize);
}
console.log('Is writable:', asString.tagInfo.isWritable);
console.log('Is writable:', info.isWritable);
}
});

Expand Down Expand Up @@ -157,6 +179,21 @@ const message: NDEFWriteOptions = {
],
};

// For complete control over binary content, use raw mode:
const rawMessage: NDEFWriteOptions = {
rawMode: true, // Bypasses automatic Text/URI formatting
records: [
{
type: 'T',
payload: 'Hello, NFC!', // Written as UTF-8 bytes without Text record prefix
},
{
type: 'custom',
payload: new Uint8Array([0x01, 0x02, 0x03, 0x04]), // Exact bytes written to tag
},
],
};

// Write NDEF message to NFC tag
NFC.writeNDEF(message)
.then(() => {
Expand Down Expand Up @@ -191,10 +228,21 @@ Returns if NFC is supported on the scanning device.

Starts the NFC scanning session on **_iOS only_**. Android devices are always in reading mode, so setting up the `nfcTag` listener is sufficient to handle tag reads on Android.

The iOS implementation now adapts automatically if the extended CoreNFC entitlements (ISO 7816, ISO 15693, FeliCa) are missing. The plugin first attempts the advanced tag reader so you can access UID/tech info. When iOS reports `Missing required entitlement`, the plugin downgrades to a compatibility mode (ISO 14443 only) and, if necessary, to the classic NDEF reader. A synthetic `nfcTag` event is emitted with `tagInfo.fallback`, `tagInfo.fallbackMode`, and `tagInfo.reason` so your UI can react immediately.

You can override the mode explicitly:

- `mode: 'auto'` (default) – advanced reader with automatic downgrade and caching.
- `mode: 'full'` – force a fresh attempt at the advanced reader, resetting cached fallback state.
- `mode: 'compat'` – skip the advanced probe and jump straight to the ISO 14443 compatibility reader.
- `mode: 'ndef'` – bypass tag sessions entirely and revert to the legacy NDEF-only reader.

Legacy booleans `forceFull`, `forceCompat`, and `forceNDEF` map to the options above for backwards compatibility.

**Returns**: `Promise<void>`

```typescript
NFC.startScan()
NFC.startScan({ mode: 'auto' })
.then(() => {
// Scanning started
})
Expand Down Expand Up @@ -233,6 +281,8 @@ Automatic formatting rules (to aid interoperability):
- Any other `type` + string payload: UTF-8 bytes only (no extra framing).
- `Uint8Array` or `number[]` payloads are treated as raw bytes and written verbatim (never altered).

**Raw Mode**: Set `rawMode: true` to bypass automatic Well Known Type formatting entirely. All string payloads will be written as UTF-8 bytes without Text ('T') or URI ('U') prefixes, giving you complete control over the binary content.

If you need full manual control of a Text or URI record, supply raw bytes (number[] / Uint8Array) and the plugin will not modify them.

If you attempt to write zero records the promise rejects with `Error("At least one NDEF record is required")`.
Expand Down Expand Up @@ -328,6 +378,11 @@ Options for writing an NDEF message.
```typescript
interface NDEFWriteOptions<T extends string | number[] | Uint8Array = string> {
records: NDEFRecord<T>[];
/**
* When true, bypasses automatic Well Known Type formatting (Text 'T' and URI 'U' prefixes).
* All payloads are written as raw bytes without additional framing.
*/
rawMode?: boolean;
}
```

Expand Down Expand Up @@ -369,12 +424,12 @@ interface TagInfo {
/**
* The unique identifier of the tag (UID) as a hex string
*/
uid: string;
uid?: string;

/**
* The NFC tag technology types supported
*/
techTypes: string[];
techTypes?: string[];

/**
* The maximum size of NDEF message that can be written to this tag (if applicable)
Expand All @@ -390,6 +445,34 @@ interface TagInfo {
* The tag type (e.g., "ISO14443-4", "MifareClassic", etc.)
*/
type?: string;

/**
* Present when the plugin downgraded capabilities for compatibility.
*/
fallback?: boolean;

/**
* Which fallback strategy is in use (`compat` or `ndef`).
*/
fallbackMode?: 'compat' | 'ndef';

/**
* Reason metadata (e.g., `missing-entitlement`).
*/
reason?: string;
}
```

#### `StartScanOptions`

Optional tweaks for the iOS reader behavior.

```typescript
interface StartScanOptions {
mode?: 'auto' | 'full' | 'compat' | 'ndef';
forceFull?: boolean;
forceCompat?: boolean;
forceNDEF?: boolean;
}
```

Expand Down
37 changes: 33 additions & 4 deletions android/src/main/kotlin/com/exxili/capacitornfc/NFCPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -186,20 +186,49 @@ class NFCPlugin : Plugin() {
return
}

val typeBytes = type.toByteArray(Charsets.UTF_8)
val payloadBytes = ByteArray(payload.length())
for(i in 0 until payload.length()) {
payloadBytes[i] = payload.getInt(i).toByte()
}

ndefRecords.add(
NdefRecord(
val (tnf, typeBytes) = when {
type == "T" || type == "U" -> Pair(
NdefRecord.TNF_WELL_KNOWN,
type.toByteArray(Charsets.UTF_8)
)
type.contains("/") -> Pair(
NdefRecord.TNF_MIME_MEDIA,
type.toByteArray(Charsets.US_ASCII)
)
else -> Pair(
NdefRecord.TNF_EXTERNAL_TYPE,
type.toByteArray(Charsets.UTF_8)
)
}

val record = if (tnf == NdefRecord.TNF_MIME_MEDIA) {
try {
NdefRecord.createMime(type, payloadBytes)
} catch (e: IllegalArgumentException) {
notifyListeners(
"nfcError",
JSObject().put(
"error",
"Invalid MIME type for record"
)
)
return
}
} else {
NdefRecord(
tnf,
typeBytes,
ByteArray(0),
payloadBytes
)
)
}

ndefRecords.add(record)
}

val ndefMessage = NdefMessage(ndefRecords.toTypedArray())
Expand Down
49 changes: 43 additions & 6 deletions dist/esm/definitions.d.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
import type { PluginListenerHandle } from '@capacitor/core';
export declare type PayloadType = string | number[] | Uint8Array;
export type PayloadType = string | number[] | Uint8Array;
export interface StartScanOptions {
/**
* Select the native reader strategy.
* - `auto` (default): attempt advanced tag session first, downgrade automatically on entitlement failures.
* - `full`: force the advanced tag session (resets any cached fallback state).
* - `compat`: force the compatibility tag session (ISO14443-only, avoids advanced entitlements).
* - `ndef`: skip tag session entirely and use the legacy NDEF reader.
*/
mode?: 'auto' | 'full' | 'compat' | 'ndef';
/**
* Backwards-compatible hints for older app code. When true, they map to `mode` selections above.
*/
forceFull?: boolean;
forceCompat?: boolean;
forceNDEF?: boolean;
}
export interface NFCPluginBasic {
/**
* Checks if NFC is supported on the device. Returns true on all iOS devices, and checks for support on Android.
*/
isSupported(): Promise<{
supported: boolean;
}>;
startScan(): Promise<void>;
/**
* Begins listening for NFC tags.
* @param options Optional tuning parameters for native reader behavior.
*/
startScan(options?: StartScanOptions): Promise<void>;
/**
* Cancels an ongoing scan session (iOS only currently; no-op / rejection on Android).
*/
Expand Down Expand Up @@ -56,11 +76,11 @@ export interface TagInfo {
/**
* The unique identifier of the tag (UID) as a hex string
*/
uid: string;
uid?: string;
/**
* The NFC tag technology types supported
*/
techTypes: string[];
techTypes?: string[];
/**
* The maximum size of NDEF message that can be written to this tag (if applicable)
*/
Expand All @@ -73,6 +93,18 @@ export interface TagInfo {
* The tag type (e.g., "ISO14443-4", "MifareClassic", etc.)
*/
type?: string;
/**
* Truthy when the plugin downgraded reader capabilities for compatibility.
*/
fallback?: boolean;
/**
* Indicates the active fallback mode (`compat` or `ndef`).
*/
fallbackMode?: 'compat' | 'ndef';
/**
* Optional reason string when fallback was applied (e.g., `missing-entitlement`).
*/
reason?: string;
}
export interface NDEFRecord<T extends PayloadType = string> {
/**
Expand All @@ -92,14 +124,19 @@ export interface NFCError {
}
export interface NDEFWriteOptions<T extends PayloadType = Uint8Array> {
records: NDEFRecord<T>[];
/**
* When true, bypasses automatic Well Known Type formatting (Text 'T' and URI 'U' prefixes).
* All payloads are written as raw bytes without additional framing.
*/
rawMode?: boolean;
}
export declare type NDEFMessagesTransformable = {
export type NDEFMessagesTransformable = {
base64: () => NDEFMessages;
uint8Array: () => NDEFMessages<Uint8Array>;
string: () => NDEFMessages;
numberArray: () => NDEFMessages<number[]>;
};
export declare type TagResultListenerFunc = (data: NDEFMessagesTransformable) => void;
export type TagResultListenerFunc = (data: NDEFMessagesTransformable) => void;
export interface NFCPlugin extends Omit<NFCPluginBasic, 'writeNDEF' | 'addListener'> {
writeNDEF: <T extends PayloadType = Uint8Array>(record?: NDEFWriteOptions<T>) => Promise<void>;
wrapperListeners: TagResultListenerFunc[];
Expand Down
Loading