diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fce56e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/node_modules +*.js \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b95bad3 --- /dev/null +++ b/README.md @@ -0,0 +1,444 @@ +# Universal NFC + +A framework-agnostic NFC package for reading and writing NFC tags in web applications and Progressive Web Apps (PWAs). This library provides a consistent API for working with NFC across different environments, with a focus on ease of use and developer experience. + +[![npm version](https://img.shields.io/npm/v/universal-nfc.svg)](https://www.npmjs.com/package/universal-nfc) +[![license](https://img.shields.io/npm/l/universal-nfc.svg)](https://github.com/yourusername/universal-nfc/blob/main/LICENSE) + +## Features + +- 📱 Read NFC tags in web apps and PWAs +- ✍️ Write data to NFC tags +- 🌐 Framework-agnostic - works with React, Vue, Angular, vanilla JS, etc. +- 📦 Lightweight, with no external dependencies +- 📊 TypeScript support with comprehensive type definitions +- 🔄 Simple and advanced APIs for different use cases +- 🔌 Based on the Web NFC API standard + +## Installation + +```bash +npm install universal-nfc +``` + +or + +```bash +yarn add universal-nfc +``` + +## Platform Compatibility + +| Feature | Chrome for Android (89+) | Chrome for Desktop | Safari iOS | Firefox | Edge | +| ----------- | ------------------------ | ------------------ | ---------- | ------- | ---- | +| Reading NFC | ✅ | ❌ | ❌ | ❌ | ❌ | +| Writing NFC | ✅ | ❌ | ❌ | ❌ | ❌ | + +**Requirements for Web NFC:** + +- Chrome 89+ on Android +- HTTPS connection (or localhost for development) +- Device with NFC hardware +- NFC enabled in device settings + +## Basic Usage + +### Reading NFC Tags (Simple API) + +The simplest way to read NFC tags: + +```javascript +import { SimpleNfc } from "universal-nfc"; + +const nfcReader = new SimpleNfc(); + +async function startReading() { + // First check if NFC is available + const available = await nfcReader.isAvailable(); + + if (!available) { + console.log("NFC is not available on this device/browser"); + return; + } + + try { + // Start reading NFC tags with a callback + await nfcReader.startReading((content, type) => { + console.log(`Read ${type} content:`, content); + + // 'type' will be 'text', 'url', or 'other' + if (type === "url") { + // Handle URL + window.open(content, "_blank"); + } else { + // Handle text or other content + document.getElementById("result").textContent = content; + } + }); + + console.log("Scan started - tap an NFC tag"); + } catch (error) { + console.error("Error starting NFC scan:", error); + } +} + +function stopReading() { + nfcReader.stopReading(); + console.log("NFC reading stopped"); +} + +// Call startReading() when your app is ready to scan +// Call stopReading() when you want to stop scanning +``` + +### Writing to NFC Tags + +```javascript +import { SimpleNfc } from "universal-nfc"; + +const nfcWriter = new SimpleNfc(); + +async function writeTextToTag() { + try { + await nfcWriter.writeText("Hello from Universal NFC!"); + console.log("Tag written successfully! Tap a tag to write."); + } catch (error) { + console.error("Error writing to tag:", error); + } +} + +async function writeUrlToTag() { + try { + await nfcWriter.writeUrl("https://example.com"); + console.log("URL written successfully! Tap a tag to write."); + } catch (error) { + console.error("Error writing URL to tag:", error); + } +} +``` + +## API Reference + +### Core API (Nfc class) + +The `Nfc` class provides comprehensive access to NFC functionality. + +```javascript +import { Nfc } from "universal-nfc"; + +const nfc = new Nfc(); +``` + +#### Methods + +- **isEnabled()**: `Promise<{enabled: boolean}>` - Check if NFC is available +- **openSettings()**: `Promise` - Open NFC settings or provide guidance +- **startScanSession(options?)**: `Promise` - Start scanning for NFC tags + - `options.once`: Stop after first tag (default: false) + - `options.scanSoundEnabled`: Play sound on detection (iOS only, default: false) + - `options.alertMessageEnabled`: Show alert on start (iOS only, default: false) +- **stopScanSession()**: `Promise` - Stop scanning for NFC tags +- **write(options)**: `Promise` - Write NDEF data to a tag +- **format()**: `Promise` - Format a tag +- **erase()**: `Promise` - Erase a tag's content +- **makeReadOnly()**: `Promise` - Make a tag read-only (permanent) +- **addListener(eventName, callback)**: `Promise<{remove: () => Promise}>` - Register event listener +- **removeAllListeners()**: `Promise` - Remove all event listeners + +#### Events + +- **tagDetected**: Fires when an NFC tag is detected +- **nfcStatusChanged**: Fires when NFC availability changes + +### Simple API (SimpleNfc class) + +The `SimpleNfc` class provides a simplified interface for common NFC operations. + +```javascript +import { SimpleNfc } from "universal-nfc"; + +const nfc = new SimpleNfc(); +``` + +#### Methods + +- **isAvailable()**: `Promise` - Check if NFC is available +- **startReading(callback)**: `Promise` - Start reading tags with a simplified callback + - `callback(content, type)`: Called with the tag's content and type ('text', 'url', or 'other') +- **stopReading()**: `Promise` - Stop reading tags +- **writeText(text)**: `Promise` - Write text to a tag +- **writeUrl(url)**: `Promise` - Write URL to a tag + +### Utilities (NfcUtils) + +```javascript +import { NfcUtils } from "universal-nfc"; +``` + +#### Methods + +- **createTextRecord(text, languageCode?)**: Create a text NDEF record +- **createUriRecord(uri)**: Create a URI NDEF record +- **createMessage(records)**: Create an NDEF message from records +- **getTextFromTag(tag)**: Extract text content from a tag +- **getUrlFromTag(tag)**: Extract URL content from a tag +- **isWebNfcSupported()**: Check if Web NFC API is supported +- **isNfcLikelyAvailable()**: Check if device likely has NFC hardware + +## Advanced Usage + +### Using the Core API for Tag Reading + +```javascript +import { Nfc } from "universal-nfc"; + +const nfc = new Nfc(); + +async function setupNfcReader() { + try { + const { enabled } = await nfc.isEnabled(); + + if (!enabled) { + console.log("NFC is not enabled or supported"); + return; + } + + // Register tag detection handler + await nfc.addListener("tagDetected", (tag) => { + console.log("Tag ID:", tag.id); + console.log("Technology types:", tag.techTypes); + + // Process NDEF messages + if (tag.messages && tag.messages.length > 0) { + for (const message of tag.messages) { + for (const record of message.records) { + console.log("Record type:", record.type); + console.log("Record payload:", record.payload); + + // Handle different record types + if (record.type === "T") { + console.log("Text:", record.text || record.payload); + } else if (record.type === "U") { + console.log("URL:", record.uri || record.payload); + } + } + } + } + }); + + // Start scanning + await nfc.startScanSession(); + console.log("NFC scanning started"); + } catch (error) { + console.error("NFC setup error:", error); + } +} + +function cleanupNfcReader() { + nfc + .stopScanSession() + .then(() => nfc.removeAllListeners()) + .then(() => { + console.log("NFC reader cleaned up"); + }) + .catch(console.error); +} +``` + +### Writing Custom Record Types + +```javascript +import { Nfc, NfcTnf, NfcUtils } from "universal-nfc"; + +const nfc = new Nfc(); + +async function writeCustomData() { + // Create a custom record + const customRecord = { + id: "", + tnf: NfcTnf.MIME_MEDIA, + type: "application/json", + payload: JSON.stringify({ id: 123, name: "Custom Data" }), + }; + + // Create a message with the custom record and a text record + const textRecord = NfcUtils.createTextRecord("This tag contains custom data"); + const message = NfcUtils.createMessage([textRecord, customRecord]); + + try { + // Write the message to a tag + await nfc.write({ message }); + console.log("Tag written successfully! Tap a tag to write."); + } catch (error) { + console.error("Error writing tag:", error); + } +} +``` + +## Handling NFC in a React Application + +```jsx +import React, { useState, useEffect } from "react"; +import { SimpleNfc } from "universal-nfc"; + +function NfcReader() { + const [isReading, setIsReading] = useState(false); + const [isAvailable, setIsAvailable] = useState(false); + const [tagContent, setTagContent] = useState(""); + const [error, setError] = useState(""); + const nfcReader = React.useMemo(() => new SimpleNfc(), []); + + useEffect(() => { + // Check if NFC is available when component mounts + nfcReader + .isAvailable() + .then((available) => { + setIsAvailable(available); + if (!available) { + setError("NFC is not available on this device or browser"); + } + }) + .catch((err) => { + setError("Error checking NFC availability: " + err.message); + }); + + // Cleanup when component unmounts + return () => { + if (isReading) { + nfcReader.stopReading().catch(console.error); + } + }; + }, [nfcReader]); + + const startReading = async () => { + try { + setError(""); + setIsReading(true); + + await nfcReader.startReading((content, type) => { + setTagContent(`${type}: ${content}`); + }); + } catch (err) { + setError("Error starting NFC: " + err.message); + setIsReading(false); + } + }; + + const stopReading = async () => { + try { + await nfcReader.stopReading(); + setIsReading(false); + } catch (err) { + setError("Error stopping NFC: " + err.message); + } + }; + + return ( +
+

NFC Reader

+ + {error &&
{error}
} + +
+ +
+ + {isReading &&

Ready to scan: tap an NFC tag against your device

} + + {tagContent && ( +
+

Tag Content:

+

{tagContent}

+
+ )} +
+ ); +} + +export default NfcReader; +``` + +## Requirements for PWA Integration + +To use NFC in a Progressive Web App (PWA): + +1. **HTTPS**: Your app must be served over HTTPS (except for localhost during development) +2. **Web App Manifest**: Include an appropriate manifest file: + +```json +{ + "name": "NFC Reader App", + "short_name": "NFC App", + "start_url": "/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#4285f4", + "icons": [ + { + "src": "icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} +``` + +3. **Service Worker**: Register a service worker to make your app work offline + +```javascript +// register-sw.js +if ("serviceWorker" in navigator) { + window.addEventListener("load", () => { + navigator.serviceWorker + .register("/service-worker.js") + .then((registration) => { + console.log("Service Worker registered"); + }) + .catch((error) => { + console.error("Service Worker registration failed:", error); + }); + }); +} +``` + +4. **Permission Policy**: You may need to include a permission policy header for NFC: + +``` +Permissions-Policy: nfc=self +``` + +## Troubleshooting + +### NFC Not Working + +1. **Check Compatibility**: Ensure you're using Chrome 89+ on Android +2. **HTTPS Required**: Make sure your app is served over HTTPS (except on localhost) +3. **NFC Hardware**: Verify your device has NFC hardware +4. **NFC Enabled**: Ensure NFC is enabled in your device settings +5. **Permission**: The user must grant permission when prompted +6. **Tag Positioning**: Position the tag correctly against the NFC sensor + +### Common Errors + +- **"NotSupportedError"**: Browser doesn't support Web NFC API +- **"NotAllowedError"**: User denied permission or NFC is disabled +- **"NetworkError"**: Problem communicating with the NFC tag +- **"AbortError"**: The operation was cancelled +- **"NotFoundError"**: No NFC tag in range or tag removed too quickly + +## License + +MIT + +## Credits + +This library is inspired by the API design of [@capawesome-team/capacitor-nfc](https://github.com/capawesome-team/capacitor-plugins/tree/main/packages/nfc), but implements a framework-agnostic solution based on the Web NFC API. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f86f26c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,33 @@ +{ + "name": "universal-nfc", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "universal-nfc", + "version": "1.0.0", + "license": "MIT", + "devDependencies": { + "typescript": "^4.9.5" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + } + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..755f097 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,103 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +}