commit 5830226e70ab63c0060581dbaf4caf98b692c30d Author: Mohamad.Elsena Date: Tue Mar 4 10:48:19 2025 +0100 weeee💃 diff --git a/definitions.ts b/definitions.ts new file mode 100644 index 0000000..ff65932 --- /dev/null +++ b/definitions.ts @@ -0,0 +1,238 @@ +export interface NfcPlugin { + /** + * Check if NFC is enabled (Android) or available (iOS/Web). + */ + isEnabled(): Promise; + + /** + * Open NFC settings (Android) or app settings (iOS) or shows guidance (Web). + */ + openSettings(): Promise; + + /** + * Register a callback that will be invoked when NFC status changes. + */ + addListener( + eventName: "nfcStatusChanged", + listenerFunc: (status: NfcStatusChangedEvent) => void + ): Promise; + + /** + * Start a read session and register a callback that will be invoked when NFC tags are detected. + */ + startScanSession(options?: StartScanSessionOptions): Promise; + + /** + * Stop the current scan session. + */ + stopScanSession(): Promise; + + /** + * Register a callback that will be invoked when NFC tags are detected. + */ + addListener( + eventName: "tagDetected", + listenerFunc: (tag: TagDetectedEvent) => void + ): Promise; + + /** + * Write an NDEF message to an NFC tag. + */ + write(options: WriteOptions): Promise; + + /** + * 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; + + /** + * Format an unformatted NFC tag. + */ + format(): Promise; + + /** + * Erase a formatted NFC tag. + */ + erase(): Promise; + + /** + * Transfer data via NFC. + * Only available on Android. + */ + share(options: ShareOptions): Promise; + + /** + * Stop sharing data via NFC. + * Only available on Android. + */ + stopSharing(): Promise; + + /** + * Remove all listeners for this plugin. + */ + removeAllListeners(): Promise; +} + +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; +} + +/** + * 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; +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..093a748 --- /dev/null +++ b/index.ts @@ -0,0 +1,5 @@ +export * from "./definitions"; +export * from "./web"; +export * from "./nfc"; +export * from "./utils"; +export * from "./simple-nfc"; diff --git a/nfc.ts b/nfc.ts new file mode 100644 index 0000000..848d952 --- /dev/null +++ b/nfc.ts @@ -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> = 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 { + 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 { + 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 { + return this.implementation.openSettings(); + } + + /** + * Start scanning for NFC tags. + * @param options Configuration options for the scan session + */ + public async startScanSession( + options?: StartScanSessionOptions + ): Promise { + 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 { + 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 { + 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 { + return this.implementation.makeReadOnly(); + } + + /** + * Format an NFC tag, erasing its contents and preparing it for writing. + */ + public async format(): Promise { + return this.implementation.format(); + } + + /** + * Erase the contents of an NFC tag. + */ + public async erase(): Promise { + 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 { + return this.implementation.share(options); + } + + /** + * Stop sharing NDEF data via NFC (Android only). + */ + public async stopSharing(): Promise { + 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; + public async addListener( + eventName: "tagDetected", + listenerFunc: (tag: TagDetectedEvent) => void + ): Promise; + public async addListener( + eventName: string, + listenerFunc: (data: any) => void + ): Promise { + // 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 { + // 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); + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6a87fae --- /dev/null +++ b/package.json @@ -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" + ] +} diff --git a/simple-nfc.ts b/simple-nfc.ts new file mode 100644 index 0000000..ea69b97 --- /dev/null +++ b/simple-nfc.ts @@ -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 { + 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 { + 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 { + this.scanCallback = null; + await this.nfc.stopScanSession(); + await this.nfc.removeAllListeners(); + } + + /** + * Write a simple text to an NFC tag + */ + async writeText(text: string): Promise { + const textRecord = NfcUtils.createTextRecord(text); + await this.nfc.write({ + message: { + records: [textRecord], + }, + }); + } + + /** + * Write a URL to an NFC tag + */ + async writeUrl(url: string): Promise { + const urlRecord = NfcUtils.createUriRecord(url); + await this.nfc.write({ + message: { + records: [urlRecord], + }, + }); + } +} diff --git a/utils.ts b/utils.ts new file mode 100644 index 0000000..768ca90 --- /dev/null +++ b/utils.ts @@ -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; + } +} diff --git a/web.ts b/web.ts new file mode 100644 index 0000000..b38f584 --- /dev/null +++ b/web.ts @@ -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 { + return { enabled: this.nfcSupported }; + } + + async openSettings(): Promise { + 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 { + 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 { + 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 { + 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 { + if (!this.nfcSupported) { + throw this.createCompatibilityError(); + } + + throw new Error("makeReadOnly is not supported in the Web NFC API"); + } + + async format(): Promise { + 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 { + // Same implementation as format for Web NFC + return this.format(); + } + + async share(options: ShareOptions): Promise { + if (!this.nfcSupported) { + throw this.createCompatibilityError(); + } + + throw new Error("NFC sharing is not supported in the Web NFC API"); + } + + async stopSharing(): Promise { + 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 { + 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 { + 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." + ); + } +}