weeee💃
This commit is contained in:
commit
5830226e70
238
definitions.ts
Normal file
238
definitions.ts
Normal file
@ -0,0 +1,238 @@
|
||||
export interface NfcPlugin {
|
||||
/**
|
||||
* Check if NFC is enabled (Android) or available (iOS/Web).
|
||||
*/
|
||||
isEnabled(): Promise<IsEnabledResult>;
|
||||
|
||||
/**
|
||||
* Open NFC settings (Android) or app settings (iOS) or shows guidance (Web).
|
||||
*/
|
||||
openSettings(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Register a callback that will be invoked when NFC status changes.
|
||||
*/
|
||||
addListener(
|
||||
eventName: "nfcStatusChanged",
|
||||
listenerFunc: (status: NfcStatusChangedEvent) => void
|
||||
): Promise<PluginListenerHandle>;
|
||||
|
||||
/**
|
||||
* Start a read session and register a callback that will be invoked when NFC tags are detected.
|
||||
*/
|
||||
startScanSession(options?: StartScanSessionOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* Stop the current scan session.
|
||||
*/
|
||||
stopScanSession(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Register a callback that will be invoked when NFC tags are detected.
|
||||
*/
|
||||
addListener(
|
||||
eventName: "tagDetected",
|
||||
listenerFunc: (tag: TagDetectedEvent) => void
|
||||
): Promise<PluginListenerHandle>;
|
||||
|
||||
/**
|
||||
* Write an NDEF message to an NFC tag.
|
||||
*/
|
||||
write(options: WriteOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* Make an NFC tag read-only. After calling this method, it is no longer possible to write data to the tag.
|
||||
* BE CAREFUL: This is a one-way process and cannot be undone.
|
||||
*/
|
||||
makeReadOnly(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Format an unformatted NFC tag.
|
||||
*/
|
||||
format(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Erase a formatted NFC tag.
|
||||
*/
|
||||
erase(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Transfer data via NFC.
|
||||
* Only available on Android.
|
||||
*/
|
||||
share(options: ShareOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* Stop sharing data via NFC.
|
||||
* Only available on Android.
|
||||
*/
|
||||
stopSharing(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Remove all listeners for this plugin.
|
||||
*/
|
||||
removeAllListeners(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IsEnabledResult {
|
||||
/**
|
||||
* Whether NFC is enabled or not.
|
||||
*/
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface NfcStatusChangedEvent {
|
||||
/**
|
||||
* Whether NFC was enabled or disabled.
|
||||
*/
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface StartScanSessionOptions {
|
||||
/**
|
||||
* If `true`, the scan session is stopped after the first tag is detected.
|
||||
* Default: `false`
|
||||
*/
|
||||
once?: boolean;
|
||||
/**
|
||||
* If `true`, a scan feedback (sound) is played when a tag is detected.
|
||||
* Only available on iOS.
|
||||
* Default: `false`
|
||||
*/
|
||||
scanSoundEnabled?: boolean;
|
||||
/**
|
||||
* If `true`, the scan session is started with alert message.
|
||||
* Only available on iOS.
|
||||
* Default: `false`
|
||||
*/
|
||||
alertMessageEnabled?: boolean;
|
||||
}
|
||||
|
||||
export interface TagDetectedEvent {
|
||||
/**
|
||||
* The ID (serial number) of the tag.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The tech types of the tag (e.g. 'ndef', 'mifare', etc.).
|
||||
*/
|
||||
techTypes: string[];
|
||||
/**
|
||||
* The NDEF messages contained in the tag.
|
||||
*/
|
||||
messages: NdefMessage[];
|
||||
}
|
||||
|
||||
export interface NdefMessage {
|
||||
/**
|
||||
* The NDEF records contained in the message.
|
||||
*/
|
||||
records: NdefRecord[];
|
||||
}
|
||||
|
||||
export interface NdefRecord {
|
||||
/**
|
||||
* The ID of the record. May be empty.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The TNF (Type Name Format) of the record.
|
||||
* @see NfcTnf for possible values.
|
||||
*/
|
||||
tnf: number;
|
||||
/**
|
||||
* The type of the record (e.g. 'T' for text, 'U' for URI).
|
||||
*/
|
||||
type: string;
|
||||
/**
|
||||
* The payload of the record as string, if available.
|
||||
*/
|
||||
payload?: string;
|
||||
/**
|
||||
* The language code of the record (for text records).
|
||||
* Only consistently available on Android.
|
||||
*/
|
||||
languageCode?: string;
|
||||
/**
|
||||
* The URI of the record (for URI records).
|
||||
*/
|
||||
uri?: string;
|
||||
/**
|
||||
* The text content of the record (for text records).
|
||||
*/
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export interface WriteOptions {
|
||||
/**
|
||||
* The NDEF message to write.
|
||||
*/
|
||||
message: NdefMessage;
|
||||
}
|
||||
|
||||
export interface ShareOptions {
|
||||
/**
|
||||
* The NDEF message to share.
|
||||
*/
|
||||
message: NdefMessage;
|
||||
}
|
||||
|
||||
export interface PluginListenerHandle {
|
||||
/**
|
||||
* Remove the listener.
|
||||
*/
|
||||
remove: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* NFC TNF (Type Name Format) Constants
|
||||
* These determine how to interpret the type field.
|
||||
*/
|
||||
export enum NfcTnf {
|
||||
EMPTY = 0x0,
|
||||
WELL_KNOWN = 0x01,
|
||||
MIME_MEDIA = 0x02,
|
||||
ABSOLUTE_URI = 0x03,
|
||||
EXTERNAL_TYPE = 0x04,
|
||||
UNKNOWN = 0x05,
|
||||
UNCHANGED = 0x06,
|
||||
RESERVED = 0x07,
|
||||
}
|
||||
|
||||
/**
|
||||
* NFC RTD (Record Type Definition) Constants
|
||||
* These are standardized type names for common record types.
|
||||
*/
|
||||
export class NfcRtd {
|
||||
public static readonly TEXT = "T";
|
||||
public static readonly URI = "U";
|
||||
public static readonly SMART_POSTER = "Sp";
|
||||
public static readonly ALTERNATIVE_CARRIER = "ac";
|
||||
public static readonly HANDOVER_CARRIER = "Hc";
|
||||
public static readonly HANDOVER_REQUEST = "Hr";
|
||||
public static readonly HANDOVER_SELECT = "Hs";
|
||||
}
|
||||
|
||||
/**
|
||||
* Error codes that might be returned by NFC operations
|
||||
*/
|
||||
export enum NfcErrorType {
|
||||
NOT_SUPPORTED = "not_supported",
|
||||
NOT_ENABLED = "not_enabled",
|
||||
PERMISSION_DENIED = "permission_denied",
|
||||
NO_TAG = "no_tag",
|
||||
TAG_ERROR = "tag_error",
|
||||
IO_ERROR = "io_error",
|
||||
TIMEOUT = "timeout",
|
||||
CANCELLED = "cancelled",
|
||||
UNEXPECTED_ERROR = "unexpected_error",
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard error structure for NFC operations
|
||||
*/
|
||||
export interface NfcError extends Error {
|
||||
code: NfcErrorType;
|
||||
message: string;
|
||||
detail?: any;
|
||||
}
|
5
index.ts
Normal file
5
index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from "./definitions";
|
||||
export * from "./web";
|
||||
export * from "./nfc";
|
||||
export * from "./utils";
|
||||
export * from "./simple-nfc";
|
218
nfc.ts
Normal file
218
nfc.ts
Normal file
@ -0,0 +1,218 @@
|
||||
import {
|
||||
NfcPlugin,
|
||||
IsEnabledResult,
|
||||
NfcStatusChangedEvent,
|
||||
StartScanSessionOptions,
|
||||
TagDetectedEvent,
|
||||
WriteOptions,
|
||||
ShareOptions,
|
||||
PluginListenerHandle,
|
||||
NfcErrorType,
|
||||
} from "./definitions";
|
||||
import { WebNfc } from "./web";
|
||||
|
||||
/**
|
||||
* Main NFC class that provides access to NFC functionality.
|
||||
* It automatically chooses the appropriate implementation for the current platform.
|
||||
*/
|
||||
export class Nfc {
|
||||
private implementation: NfcPlugin;
|
||||
private listeners: Map<string, Set<Function>> = new Map();
|
||||
|
||||
constructor() {
|
||||
// Currently we only have the Web implementation
|
||||
this.implementation = new WebNfc();
|
||||
|
||||
// Set up status monitoring to track NFC availability
|
||||
this.monitorNfcStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to monitor NFC status changes
|
||||
*/
|
||||
private async monitorNfcStatus(): Promise<void> {
|
||||
try {
|
||||
// Set up listener for NFC status changes from implementation
|
||||
await this.implementation.addListener("nfcStatusChanged", (status) => {
|
||||
// Propagate to our own listeners
|
||||
this.notifyListeners("nfcStatusChanged", status);
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("Failed to set up NFC status monitoring:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if NFC is enabled (Android) or available (iOS/Web).
|
||||
* @returns Promise resolving to an object with an `enabled` boolean property
|
||||
*/
|
||||
public async isEnabled(): Promise<IsEnabledResult> {
|
||||
try {
|
||||
return await this.implementation.isEnabled();
|
||||
} catch (error) {
|
||||
console.error("Error checking NFC status:", error);
|
||||
return { enabled: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open NFC settings (Android) or app settings (iOS) or shows guidance (Web).
|
||||
* This helps users enable NFC if it's disabled.
|
||||
*/
|
||||
public async openSettings(): Promise<void> {
|
||||
return this.implementation.openSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start scanning for NFC tags.
|
||||
* @param options Configuration options for the scan session
|
||||
*/
|
||||
public async startScanSession(
|
||||
options?: StartScanSessionOptions
|
||||
): Promise<void> {
|
||||
try {
|
||||
return await this.implementation.startScanSession(options);
|
||||
} catch (error: any) {
|
||||
// Convert to a standardized error object if possible
|
||||
const nfcError: any = new Error(
|
||||
error.message || "Failed to start NFC scan"
|
||||
);
|
||||
|
||||
if (
|
||||
error.message?.includes("not supported") ||
|
||||
error.name === "NotSupportedError"
|
||||
) {
|
||||
nfcError.code = NfcErrorType.NOT_SUPPORTED;
|
||||
} else if (
|
||||
error.message?.includes("permission") ||
|
||||
error.name === "NotAllowedError"
|
||||
) {
|
||||
nfcError.code = NfcErrorType.PERMISSION_DENIED;
|
||||
} else if (error.message?.includes("not enabled")) {
|
||||
nfcError.code = NfcErrorType.NOT_ENABLED;
|
||||
} else {
|
||||
nfcError.code = NfcErrorType.UNEXPECTED_ERROR;
|
||||
}
|
||||
|
||||
throw nfcError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the current NFC scan session.
|
||||
*/
|
||||
public async stopScanSession(): Promise<void> {
|
||||
return this.implementation.stopScanSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an NDEF message to an NFC tag.
|
||||
* @param options Object containing the NDEF message to write
|
||||
*/
|
||||
public async write(options: WriteOptions): Promise<void> {
|
||||
return this.implementation.write(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an NFC tag read-only.
|
||||
* WARNING: This is a permanent operation that cannot be undone.
|
||||
*/
|
||||
public async makeReadOnly(): Promise<void> {
|
||||
return this.implementation.makeReadOnly();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an NFC tag, erasing its contents and preparing it for writing.
|
||||
*/
|
||||
public async format(): Promise<void> {
|
||||
return this.implementation.format();
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase the contents of an NFC tag.
|
||||
*/
|
||||
public async erase(): Promise<void> {
|
||||
return this.implementation.erase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Share NDEF data via NFC (Android only, not available on iOS or Web).
|
||||
* @param options Object containing the NDEF message to share
|
||||
*/
|
||||
public async share(options: ShareOptions): Promise<void> {
|
||||
return this.implementation.share(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop sharing NDEF data via NFC (Android only).
|
||||
*/
|
||||
public async stopSharing(): Promise<void> {
|
||||
return this.implementation.stopSharing();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an event listener.
|
||||
* @param eventName Name of the event to listen for
|
||||
* @param listenerFunc Callback function to invoke when the event occurs
|
||||
* @returns A handle that can be used to remove the listener
|
||||
*/
|
||||
public async addListener(
|
||||
eventName: "nfcStatusChanged",
|
||||
listenerFunc: (status: NfcStatusChangedEvent) => void
|
||||
): Promise<PluginListenerHandle>;
|
||||
public async addListener(
|
||||
eventName: "tagDetected",
|
||||
listenerFunc: (tag: TagDetectedEvent) => void
|
||||
): Promise<PluginListenerHandle>;
|
||||
public async addListener(
|
||||
eventName: string,
|
||||
listenerFunc: (data: any) => void
|
||||
): Promise<PluginListenerHandle> {
|
||||
// Add to our internal listener registry
|
||||
if (!this.listeners.has(eventName)) {
|
||||
this.listeners.set(eventName, new Set());
|
||||
}
|
||||
this.listeners.get(eventName)?.add(listenerFunc);
|
||||
|
||||
// Register with the implementation
|
||||
const handle = await this.implementation.addListener(
|
||||
eventName as "nfcStatusChanged" | "tagDetected" as any,
|
||||
listenerFunc
|
||||
);
|
||||
|
||||
// Return a handle that will clean up properly
|
||||
return {
|
||||
remove: async () => {
|
||||
this.listeners.get(eventName)?.delete(listenerFunc);
|
||||
return handle.remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all event listeners registered for this plugin.
|
||||
*/
|
||||
public async removeAllListeners(): Promise<void> {
|
||||
// Clear our internal listener registry
|
||||
this.listeners.clear();
|
||||
|
||||
// Remove listeners from the implementation
|
||||
return this.implementation.removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to notify listeners of events
|
||||
*/
|
||||
private notifyListeners(eventName: string, data: any): void {
|
||||
const listeners = this.listeners.get(eventName);
|
||||
if (listeners) {
|
||||
for (const listener of listeners) {
|
||||
try {
|
||||
listener(data);
|
||||
} catch (error) {
|
||||
console.error(`Error in ${eventName} listener:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
package.json
Normal file
35
package.json
Normal file
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "universal-nfc",
|
||||
"version": "1.0.0",
|
||||
"description": "A framework-agnostic NFC package for reading/writing NFC tags in web apps and PWAs",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"prepare": "npm run build",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [
|
||||
"nfc",
|
||||
"ndef",
|
||||
"rfid",
|
||||
"web nfc",
|
||||
"pwa",
|
||||
"mobile"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/yourusername/universal-nfc.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"chrome >= 89"
|
||||
]
|
||||
}
|
97
simple-nfc.ts
Normal file
97
simple-nfc.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { Nfc } from "./nfc";
|
||||
import { NfcUtils } from "./utils";
|
||||
|
||||
/**
|
||||
* A simplified API for common NFC reading operations
|
||||
*/
|
||||
export class SimpleNfc {
|
||||
private nfc: Nfc;
|
||||
private scanCallback: ((text: string, type: string) => void) | null = null;
|
||||
|
||||
constructor() {
|
||||
this.nfc = new Nfc();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if NFC is available on this device/browser
|
||||
*/
|
||||
async isAvailable(): Promise<boolean> {
|
||||
try {
|
||||
const result = await this.nfc.isEnabled();
|
||||
return result.enabled;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start scanning for NFC tags with a simplified callback
|
||||
* The callback will receive text content and content type ('text', 'url', or 'other')
|
||||
*/
|
||||
async startReading(
|
||||
callback: (content: string, type: string) => void
|
||||
): Promise<void> {
|
||||
this.scanCallback = callback;
|
||||
|
||||
// Set up the tag detection listener
|
||||
await this.nfc.addListener("tagDetected", (tag) => {
|
||||
// Try to get URL first
|
||||
const url = NfcUtils.getUrlFromTag(tag);
|
||||
if (url) {
|
||||
this.scanCallback?.(url, "url");
|
||||
return;
|
||||
}
|
||||
|
||||
// Then try to get text
|
||||
const text = NfcUtils.getTextFromTag(tag);
|
||||
if (text) {
|
||||
this.scanCallback?.(text, "text");
|
||||
return;
|
||||
}
|
||||
|
||||
// If we got here, we have other content
|
||||
if (tag.messages.length > 0 && tag.messages[0].records.length > 0) {
|
||||
const firstRecord = tag.messages[0].records[0];
|
||||
this.scanCallback?.(firstRecord.payload || "Unknown content", "other");
|
||||
} else {
|
||||
this.scanCallback?.("Empty tag", "other");
|
||||
}
|
||||
});
|
||||
|
||||
// Start the actual scan
|
||||
await this.nfc.startScanSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop scanning for NFC tags
|
||||
*/
|
||||
async stopReading(): Promise<void> {
|
||||
this.scanCallback = null;
|
||||
await this.nfc.stopScanSession();
|
||||
await this.nfc.removeAllListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a simple text to an NFC tag
|
||||
*/
|
||||
async writeText(text: string): Promise<void> {
|
||||
const textRecord = NfcUtils.createTextRecord(text);
|
||||
await this.nfc.write({
|
||||
message: {
|
||||
records: [textRecord],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a URL to an NFC tag
|
||||
*/
|
||||
async writeUrl(url: string): Promise<void> {
|
||||
const urlRecord = NfcUtils.createUriRecord(url);
|
||||
await this.nfc.write({
|
||||
message: {
|
||||
records: [urlRecord],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
100
utils.ts
Normal file
100
utils.ts
Normal file
@ -0,0 +1,100 @@
|
||||
// src/utils.ts
|
||||
import { NdefRecord, NdefMessage, NfcTnf, NfcRtd } from "./definitions";
|
||||
|
||||
export class NfcUtils {
|
||||
/**
|
||||
* Creates a simple text record
|
||||
*/
|
||||
static createTextRecord(
|
||||
text: string,
|
||||
languageCode: string = "en"
|
||||
): NdefRecord {
|
||||
return {
|
||||
id: "",
|
||||
tnf: NfcTnf.WELL_KNOWN,
|
||||
type: NfcRtd.TEXT,
|
||||
payload: text,
|
||||
languageCode,
|
||||
text,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a URI/URL record
|
||||
*/
|
||||
static createUriRecord(uri: string): NdefRecord {
|
||||
return {
|
||||
id: "",
|
||||
tnf: NfcTnf.WELL_KNOWN,
|
||||
type: NfcRtd.URI,
|
||||
payload: uri,
|
||||
uri,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a complete NDEF message with one or more records
|
||||
*/
|
||||
static createMessage(records: NdefRecord[]): NdefMessage {
|
||||
return { records };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract text content from detected tag
|
||||
* Returns the first text record found or null if none
|
||||
*/
|
||||
static getTextFromTag(tag: { messages: NdefMessage[] }): string | null {
|
||||
for (const message of tag.messages) {
|
||||
for (const record of message.records) {
|
||||
// First check the text field
|
||||
if (record.text) {
|
||||
return record.text;
|
||||
}
|
||||
// Then check for text record type
|
||||
if ((record.type === "T" || record.type === "text") && record.payload) {
|
||||
return record.payload;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract URL/URI from detected tag
|
||||
* Returns the first URL record found or null if none
|
||||
*/
|
||||
static getUrlFromTag(tag: { messages: NdefMessage[] }): string | null {
|
||||
for (const message of tag.messages) {
|
||||
for (const record of message.records) {
|
||||
// First check uri field
|
||||
if (record.uri) {
|
||||
return record.uri;
|
||||
}
|
||||
// Then check for URI record type
|
||||
if ((record.type === "U" || record.type === "url") && record.payload) {
|
||||
return record.payload;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this browser environment supports Web NFC
|
||||
*/
|
||||
static isWebNfcSupported(): boolean {
|
||||
return typeof window !== "undefined" && "NDEFReader" in window;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the device is likely to have NFC hardware
|
||||
* (Not 100% reliable but useful as a hint)
|
||||
*/
|
||||
static isNfcLikelyAvailable(): boolean {
|
||||
const ua = navigator.userAgent;
|
||||
// Most Android devices have NFC these days
|
||||
if (/android/i.test(ua)) return true;
|
||||
// iOS detection is tricky as Web NFC isn't available anyway
|
||||
return false;
|
||||
}
|
||||
}
|
371
web.ts
Normal file
371
web.ts
Normal file
@ -0,0 +1,371 @@
|
||||
import {
|
||||
NfcPlugin,
|
||||
IsEnabledResult,
|
||||
StartScanSessionOptions,
|
||||
WriteOptions,
|
||||
ShareOptions,
|
||||
PluginListenerHandle,
|
||||
TagDetectedEvent,
|
||||
NdefRecord,
|
||||
} from "./definitions";
|
||||
|
||||
export class WebNfc implements NfcPlugin {
|
||||
private scanSessionActive = false;
|
||||
private scanOnce = false;
|
||||
private listeners: { [key: string]: Array<(...args: any[]) => void> } = {};
|
||||
private ndefReader: any = null;
|
||||
private nfcSupported: boolean = false;
|
||||
|
||||
constructor() {
|
||||
this.detectNfcSupport();
|
||||
}
|
||||
|
||||
private detectNfcSupport() {
|
||||
// Check if Web NFC API is available
|
||||
if (typeof window !== "undefined") {
|
||||
// Primary check for NDEFReader
|
||||
this.nfcSupported = "NDEFReader" in window;
|
||||
|
||||
// Log platform info for debugging
|
||||
const userAgent = navigator.userAgent;
|
||||
const isHttps = window.location.protocol === "https:";
|
||||
const isPWA = window.matchMedia("(display-mode: standalone)").matches;
|
||||
|
||||
console.info("NFC Support Check:", {
|
||||
supported: this.nfcSupported,
|
||||
isHttps: isHttps,
|
||||
isPWA: isPWA,
|
||||
isAndroid: /android/i.test(userAgent),
|
||||
isIOS: /iphone|ipad|ipod/i.test(userAgent),
|
||||
isChrome: /chrome/i.test(userAgent),
|
||||
});
|
||||
|
||||
if (!this.nfcSupported) {
|
||||
// Provide helpful message about browser compatibility
|
||||
const reason = !isHttps
|
||||
? "NFC requires HTTPS"
|
||||
: !/android/i.test(userAgent)
|
||||
? "NFC Web API only supported on Android"
|
||||
: !/chrome/i.test(userAgent)
|
||||
? "NFC Web API only supported in Chrome-based browsers"
|
||||
: "This browser does not support the Web NFC API";
|
||||
|
||||
console.warn(`Web NFC not available: ${reason}`);
|
||||
}
|
||||
} else {
|
||||
this.nfcSupported = false;
|
||||
console.warn("Web NFC not available: Not in browser environment");
|
||||
}
|
||||
}
|
||||
|
||||
async isEnabled(): Promise<IsEnabledResult> {
|
||||
return { enabled: this.nfcSupported };
|
||||
}
|
||||
|
||||
async openSettings(): Promise<void> {
|
||||
if (!this.nfcSupported) {
|
||||
throw this.createCompatibilityError();
|
||||
}
|
||||
|
||||
console.warn(
|
||||
"openSettings: On web, users must enable NFC in device settings manually."
|
||||
);
|
||||
|
||||
// Provide instructions based on browser detection
|
||||
if (/android/i.test(navigator.userAgent)) {
|
||||
alert(
|
||||
"Please enable NFC in your device settings: Settings > Connected devices > Connection preferences > NFC"
|
||||
);
|
||||
} else {
|
||||
alert("Please ensure NFC is enabled on your device.");
|
||||
}
|
||||
}
|
||||
|
||||
async startScanSession(options?: StartScanSessionOptions): Promise<void> {
|
||||
if (!this.nfcSupported) {
|
||||
throw this.createCompatibilityError();
|
||||
}
|
||||
|
||||
this.scanSessionActive = true;
|
||||
this.scanOnce = options?.once ?? false;
|
||||
|
||||
try {
|
||||
// Create and configure the NDEF reader
|
||||
this.ndefReader = new (window as any).NDEFReader();
|
||||
|
||||
// Set up event listeners
|
||||
this.ndefReader.addEventListener("reading", (event: any) => {
|
||||
if (!this.scanSessionActive) return;
|
||||
|
||||
try {
|
||||
const tag = this.parseNdefReading(event);
|
||||
|
||||
// Notify listeners
|
||||
const tagListeners = this.listeners["tagDetected"] || [];
|
||||
for (const listener of tagListeners) {
|
||||
listener(tag);
|
||||
}
|
||||
|
||||
// If scanOnce is true, stop scanning after first detection
|
||||
if (this.scanOnce) {
|
||||
this.stopScanSession();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error processing NFC tag:", error);
|
||||
}
|
||||
});
|
||||
|
||||
this.ndefReader.addEventListener("error", (error: any) => {
|
||||
console.error(`NFC Error: ${error.message || error}`);
|
||||
});
|
||||
|
||||
// Start scanning - might throw if user denies permission
|
||||
await this.ndefReader.scan();
|
||||
console.log("NFC scan started successfully");
|
||||
} catch (error: any) {
|
||||
this.scanSessionActive = false;
|
||||
|
||||
// Provide user-friendly error message
|
||||
if (error.name === "NotAllowedError") {
|
||||
console.error("NFC permission denied by user");
|
||||
throw new Error(
|
||||
"NFC permission denied. Please allow NFC scanning when prompted."
|
||||
);
|
||||
} else if (error.name === "NotSupportedError") {
|
||||
console.error("NFC not supported on this device/browser");
|
||||
throw new Error("NFC is not supported on this device or browser.");
|
||||
} else {
|
||||
console.error("Error starting NFC scan:", error);
|
||||
throw new Error(
|
||||
`Failed to start NFC scan: ${error.message || "Unknown error"}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private parseNdefReading(event: any): TagDetectedEvent {
|
||||
const serialNumber = event.serialNumber || "";
|
||||
const messages: { records: NdefRecord[] }[] = [];
|
||||
|
||||
if (event.message) {
|
||||
const records: NdefRecord[] = [];
|
||||
|
||||
// Parse each NDEF record
|
||||
for (const record of event.message.records) {
|
||||
const recordType = record.recordType;
|
||||
let payload = "";
|
||||
let text = "";
|
||||
let uri = "";
|
||||
|
||||
// Handle different record types
|
||||
if (record.data) {
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
if (recordType === "text") {
|
||||
// Text record
|
||||
text = decoder.decode(record.data);
|
||||
payload = text;
|
||||
} else if (recordType === "url") {
|
||||
// URL record
|
||||
uri = decoder.decode(record.data);
|
||||
payload = uri;
|
||||
} else {
|
||||
// Other record types - try to decode as text
|
||||
try {
|
||||
payload = decoder.decode(record.data);
|
||||
} catch (e) {
|
||||
// If text decoding fails, get hex representation
|
||||
payload = Array.from(new Uint8Array(record.data))
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create record in our format
|
||||
records.push({
|
||||
id: "",
|
||||
tnf: this.mapRecordTypeToTnf(recordType),
|
||||
type: recordType || "",
|
||||
payload,
|
||||
languageCode: record.lang || "en",
|
||||
text,
|
||||
uri,
|
||||
});
|
||||
}
|
||||
|
||||
if (records.length > 0) {
|
||||
messages.push({ records });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: serialNumber,
|
||||
techTypes: ["ndef"], // Web NFC only exposes NDEF
|
||||
messages,
|
||||
};
|
||||
}
|
||||
|
||||
private mapRecordTypeToTnf(recordType: string): number {
|
||||
// Map Web NFC record types to TNF values
|
||||
if (!recordType) return 0; // EMPTY
|
||||
if (recordType === "text" || recordType === "url") return 1; // WELL_KNOWN
|
||||
if (recordType.includes("/")) return 2; // MIME_MEDIA
|
||||
if (recordType.startsWith("urn:")) return 3; // ABSOLUTE_URI
|
||||
return 4; // EXTERNAL_TYPE (default)
|
||||
}
|
||||
|
||||
async stopScanSession(): Promise<void> {
|
||||
this.scanSessionActive = false;
|
||||
|
||||
if (this.ndefReader) {
|
||||
try {
|
||||
// While Web NFC doesn't have explicit stop method, we can
|
||||
// use AbortController in newer implementations
|
||||
if (this.ndefReader.abort) {
|
||||
this.ndefReader.abort();
|
||||
}
|
||||
console.log("NFC scan stopped");
|
||||
} catch (error) {
|
||||
console.warn("Error stopping NFC scan:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async write(options: WriteOptions): Promise<void> {
|
||||
if (!this.nfcSupported) {
|
||||
throw this.createCompatibilityError();
|
||||
}
|
||||
|
||||
try {
|
||||
const writer = new (window as any).NDEFReader();
|
||||
|
||||
// Convert our message format to Web NFC format
|
||||
const records = options.message.records.map((record) => {
|
||||
const recordOptions: any = {};
|
||||
|
||||
// Handle different record types
|
||||
if (record.type === "T") {
|
||||
recordOptions.recordType = "text";
|
||||
recordOptions.data = record.payload || record.text || "";
|
||||
if (record.languageCode) {
|
||||
recordOptions.lang = record.languageCode;
|
||||
}
|
||||
} else if (record.type === "U") {
|
||||
recordOptions.recordType = "url";
|
||||
recordOptions.data = record.payload || record.uri || "";
|
||||
} else {
|
||||
// For other types
|
||||
recordOptions.recordType = record.type;
|
||||
recordOptions.data = record.payload || "";
|
||||
}
|
||||
|
||||
return recordOptions;
|
||||
});
|
||||
|
||||
// Write the records
|
||||
await writer.write({ records });
|
||||
console.log("NFC write successful");
|
||||
} catch (error: any) {
|
||||
if (error.name === "NotAllowedError") {
|
||||
throw new Error(
|
||||
"NFC write permission denied. Please allow when prompted."
|
||||
);
|
||||
} else if (error.name === "NotSupportedError") {
|
||||
throw new Error(
|
||||
"NFC write is not supported on this device or browser."
|
||||
);
|
||||
} else {
|
||||
console.error("Error writing to NFC tag:", error);
|
||||
throw new Error(
|
||||
`Failed to write to NFC tag: ${error.message || "Unknown error"}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async makeReadOnly(): Promise<void> {
|
||||
if (!this.nfcSupported) {
|
||||
throw this.createCompatibilityError();
|
||||
}
|
||||
|
||||
throw new Error("makeReadOnly is not supported in the Web NFC API");
|
||||
}
|
||||
|
||||
async format(): Promise<void> {
|
||||
if (!this.nfcSupported) {
|
||||
throw this.createCompatibilityError();
|
||||
}
|
||||
|
||||
// To "format" in Web NFC, write an empty NDEF message
|
||||
try {
|
||||
const writer = new (window as any).NDEFReader();
|
||||
await writer.write({ records: [] });
|
||||
console.log("NFC tag formatted (wrote empty NDEF message)");
|
||||
} catch (error: any) {
|
||||
console.error("Error formatting NFC tag:", error);
|
||||
throw new Error(
|
||||
`Failed to format NFC tag: ${error.message || "Unknown error"}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async erase(): Promise<void> {
|
||||
// Same implementation as format for Web NFC
|
||||
return this.format();
|
||||
}
|
||||
|
||||
async share(options: ShareOptions): Promise<void> {
|
||||
if (!this.nfcSupported) {
|
||||
throw this.createCompatibilityError();
|
||||
}
|
||||
|
||||
throw new Error("NFC sharing is not supported in the Web NFC API");
|
||||
}
|
||||
|
||||
async stopSharing(): Promise<void> {
|
||||
if (!this.nfcSupported) {
|
||||
throw this.createCompatibilityError();
|
||||
}
|
||||
|
||||
throw new Error("NFC sharing is not supported in the Web NFC API");
|
||||
}
|
||||
|
||||
async addListener(
|
||||
eventName: "nfcStatusChanged" | "tagDetected",
|
||||
listenerFunc: (data: any) => void
|
||||
): Promise<PluginListenerHandle> {
|
||||
if (!this.listeners[eventName]) {
|
||||
this.listeners[eventName] = [];
|
||||
}
|
||||
|
||||
this.listeners[eventName].push(listenerFunc);
|
||||
|
||||
return {
|
||||
remove: async () => {
|
||||
this.removeListener(eventName, listenerFunc);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private removeListener(
|
||||
eventName: string,
|
||||
listenerFunc: (data: any) => void
|
||||
): void {
|
||||
if (this.listeners[eventName]) {
|
||||
this.listeners[eventName] = this.listeners[eventName].filter(
|
||||
(listener) => listener !== listenerFunc
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async removeAllListeners(): Promise<void> {
|
||||
this.listeners = {};
|
||||
}
|
||||
|
||||
private createCompatibilityError(): Error {
|
||||
return new Error(
|
||||
"Web NFC API is not supported in this browser. NFC Web API requires Chrome 89+ on Android with NFC hardware, running over HTTPS."
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user