From 37fc6f8b5722d855c0d397e522476f5fd696b76d Mon Sep 17 00:00:00 2001 From: mohamad Date: Tue, 4 Mar 2025 21:32:53 +0100 Subject: [PATCH] added ios support --- README.md | 283 ++++++++++++++++++++++++++++++++++++++++------- ios-bridge.ts | 174 +++++++++++++++++++++++++++++ ios-detection.ts | 73 ++++++++++++ nfc.ts | 15 ++- web.ts | 105 +++++++++++------- 5 files changed, 566 insertions(+), 84 deletions(-) create mode 100644 ios-bridge.ts create mode 100644 ios-detection.ts diff --git a/README.md b/README.md index b95bad3..3ec3f3c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # 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. +A framework-agnostic NFC package for reading and writing NFC tags in web applications and Progressive Web Apps (PWAs), with iOS support options. 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) @@ -14,6 +14,7 @@ A framework-agnostic NFC package for reading and writing NFC tags in web applica - 📊 TypeScript support with comprehensive type definitions - 🔄 Simple and advanced APIs for different use cases - 🔌 Based on the Web NFC API standard +- 🍎 iOS support via native bridge integration ## Installation @@ -29,10 +30,15 @@ yarn add universal-nfc ## Platform Compatibility -| Feature | Chrome for Android (89+) | Chrome for Desktop | Safari iOS | Firefox | Edge | -| ----------- | ------------------------ | ------------------ | ---------- | ------- | ---- | -| Reading NFC | ✅ | ❌ | ❌ | ❌ | ❌ | -| Writing NFC | ✅ | ❌ | ❌ | ❌ | ❌ | +| Feature | Chrome for Android (89+) | Chrome for Desktop | Safari iOS | Native iOS Apps | Native Android Apps | Firefox | Edge | +|--------------|------------------------|------------------|-----------|----------------|----------------|---------|------| +| Reading NFC | ✅ | ❌ | ❌ | ✅* | ✅ | ❌ | ❌ | +| Writing NFC | ✅ | ❌ | ❌ | ✅* | ✅ | ❌ | ❌ | + +\*iOS support requires a native app with an embedded WebView and bridge implementation. + + + **Requirements for Web NFC:** @@ -41,6 +47,15 @@ yarn add universal-nfc - Device with NFC hardware - NFC enabled in device settings + + +**Requirements for iOS NFC:** + +- iOS 11+ device with NFC hardware +- Native iOS app with NFC entitlements +- Core NFC framework integration +- Custom bridge implementation (see iOS Integration section) + ## Basic Usage ### Reading NFC Tags (Simple API) @@ -48,47 +63,45 @@ yarn add universal-nfc The simplest way to read NFC tags: ```javascript -import { SimpleNfc } from "universal-nfc"; +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); - } + // 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"); + 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 @@ -117,6 +130,196 @@ async function writeUrlToTag() { } ``` +## iOS Integration + +### Overview + +iOS devices with NFC hardware (iPhone 7 and later running iOS 11+) support reading NFC tags, but with some important restrictions: + +- **Safari browser cannot directly access NFC**: The Web NFC API is not supported in Safari. +- **Native app required**: NFC on iOS requires a native app built with the Core NFC framework. +- **Web apps can access NFC through a bridge**: Web content running in a WebView inside a native app can access NFC via a custom bridge. + +### iOS Native Bridge Setup + +To use Universal NFC with iOS, you need to: + +1. Create a native iOS app with WebView. +2. Implement Core NFC. +3. Create a JavaScript bridge. + +#### 1. Native iOS App with NFC Capabilities + +First, ensure your app has NFC entitlements: + +- Add the NFC entitlement to your app in Xcode. +- Add `NFCReaderUsageDescription` in `Info.plist`. +- Enable the "Near Field Communication Tag Reading" capability. + +#### 2. Implement the NFC Bridge in Swift + +```swift +import WebKit +import CoreNFC + +class NfcBridge: NSObject, NFCNDEFReaderSessionDelegate { + weak var webView: WKWebView? + var nfcSession: NFCNDEFReaderSession? + + init(webView: WKWebView) { + self.webView = webView + super.init() + + // Register JavaScript interface + let bridgeScript = WKUserScript( + source: "window.nativeNfcBridge = { + isNfcEnabled: function() { return window.webkit.messageHandlers.nfcBridge.postMessage({action: 'isEnabled'}); }, + startNfcScan: function(options) { return window.webkit.messageHandlers.nfcBridge.postMessage({action: 'startScan', options: options}); }, + stopNfcScan: function() { return window.webkit.messageHandlers.nfcBridge.postMessage({action: 'stopScan'}); }, + writeNfcTag: function(data) { return window.webkit.messageHandlers.nfcBridge.postMessage({action: 'writeTag', data: data}); }, + openSettings: function() { return window.webkit.messageHandlers.nfcBridge.postMessage({action: 'openSettings'}); } + };", + injectionTime: .atDocumentStart, + forMainFrameOnly: false + ) + + let contentController = webView.configuration.userContentController + contentController.addUserScript(bridgeScript) + contentController.add(self, name: "nfcBridge") + } + + // Handle JavaScript messages + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + guard let body = message.body as? [String: Any], + let action = body["action"] as? String else { + return + } + + switch action { + case "isEnabled": + checkNfcAvailability() + case "startScan": + let options = body["options"] as? String ?? "{}" + startNfcSession(options: options) + case "stopScan": + stopNfcSession() + case "writeTag": + if let data = body["data"] as? String { + writeNfcTag(data: data) + } + case "openSettings": + openSettings() + default: + break + } + } + + private func checkNfcAvailability() { + let isAvailable = NFCNDEFReaderSession.readingAvailable + sendToWebView(script: "window.dispatchEvent(new MessageEvent('message', {data: {type: 'nfcStatusChanged', enabled: \(isAvailable)}}));") + } + + private func startNfcSession(options: String) { + guard NFCNDEFReaderSession.readingAvailable else { + sendToWebView(script: "console.error('NFC reading not available on this device');") + return + } + + nfcSession = NFCNDEFReaderSession(delegate: self, queue: nil, invalidateAfterFirstRead: false) + nfcSession?.alertMessage = "Hold your iPhone near an NFC tag" + nfcSession?.begin() + } + + private func stopNfcSession() { + nfcSession?.invalidate() + nfcSession = nil + } + + private func writeNfcTag(data: String) { + // Implement write logic + } + + private func openSettings() { + if let url = URL(string: UIApplication.openSettingsURLString) { + DispatchQueue.main.async { + UIApplication.shared.open(url) + } + } + } + + private func sendToWebView(script: String) { + DispatchQueue.main.async { + self.webView?.evaluateJavaScript(script, completionHandler: nil) + } + } +} +``` + +#### 3. Initialize the Bridge in Your View Controller + +```swift +import UIKit +import WebKit + +class WebViewController: UIViewController { + var webView: WKWebView! + var nfcBridge: NfcBridge! + + override func viewDidLoad() { + super.viewDidLoad() + + webView = WKWebView(frame: view.bounds) + webView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + view.addSubview(webView) + + nfcBridge = NfcBridge(webView: webView) + + if let url = URL(string: "https://your-web-app-url.com") { + webView.load(URLRequest(url: url)) + } + } +} +``` + +### Using the iOS Bridge in Your Web App + +The native bridge will automatically be detected by Universal NFC when your web app runs inside the native iOS app's WebView: + +```javascript +import { Nfc } from 'universal-nfc'; + +const nfc = new Nfc(); + +async function checkNfcSupport() { + const { enabled } = await nfc.isEnabled(); + console.log('NFC supported and enabled:', enabled); + + if (enabled) { + await nfc.startScanSession(); + + await nfc.addListener('tagDetected', (tag) => { + console.log('NFC tag detected via iOS bridge:', tag); + }); + } +} +``` + +### Troubleshooting iOS-Specific Issues + +#### "NFC reading is not supported in web browsers on iOS" +- This error occurs when trying to use NFC in Safari or a standard WebView. +- **Solution**: Use the native bridge approach with a custom iOS app. + +#### "CoreNFC framework missing" +- Make sure you have the proper entitlements in your iOS app. +- Check that your iOS device supports NFC (iPhone 7 and later). + +#### "Bridge communication error" +- Check the bridge implementation in your native app. +- Verify message format passed between WebView and native code. + + + ## API Reference ### Core API (Nfc class) @@ -437,8 +640,4 @@ Permissions-Policy: nfc=self ## 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. +MIT \ No newline at end of file diff --git a/ios-bridge.ts b/ios-bridge.ts new file mode 100644 index 0000000..730de26 --- /dev/null +++ b/ios-bridge.ts @@ -0,0 +1,174 @@ +import { NfcPlugin, IsEnabledResult, TagDetectedEvent } from './definitions.js'; + +/** + * Interface for native iOS app to implement for WebView communication + */ +interface NativeIosBridge { + isNfcEnabled?: () => Promise; + startNfcScan?: (options: string) => Promise; + stopNfcScan?: () => Promise; + writeNfcTag?: (data: string) => Promise; + openSettings?: () => Promise; +} + +/** + * NFC implementation that communicates with a native iOS app via a bridge + * Note: This requires custom implementation in a native iOS app + */ +export class IosBridgeNfc implements NfcPlugin { + private bridge: NativeIosBridge; + private listeners: { [key: string]: Array<(...args: any[]) => void> } = {}; + private isBridgeAvailable: boolean = false; + + constructor() { + // Look for the bridge in the global scope + // The native app needs to inject this object + this.bridge = (window as any).nativeNfcBridge || {}; + this.isBridgeAvailable = !!(this.bridge.isNfcEnabled && this.bridge.startNfcScan); + + // Set up message listener for tag detection events from native app + if (this.isBridgeAvailable) { + window.addEventListener('message', this.handleNativeMessage.bind(this)); + console.log('iOS NFC bridge initialized'); + } else { + console.warn('iOS NFC bridge not found'); + } + } + + private handleNativeMessage(event: MessageEvent) { + if (!event.data || typeof event.data !== 'object') return; + + // Handle tag detected message from native app + if (event.data.type === 'nfcTagDetected' && event.data.tag) { + const tagListeners = this.listeners['tagDetected'] || []; + const tag = event.data.tag as TagDetectedEvent; + + for (const listener of tagListeners) { + listener(tag); + } + } + + // Handle NFC status change from native app + if (event.data.type === 'nfcStatusChanged' && event.data.enabled !== undefined) { + const statusListeners = this.listeners['nfcStatusChanged'] || []; + + for (const listener of statusListeners) { + listener({ enabled: !!event.data.enabled }); + } + } + } + + async isEnabled(): Promise { + if (!this.isBridgeAvailable) { + return { enabled: false }; + } + + try { + const enabled = await this.bridge.isNfcEnabled!(); + return { enabled }; + } catch (error) { + console.error('Error checking NFC status through bridge:', error); + return { enabled: false }; + } + } + + async openSettings(): Promise { + if (!this.isBridgeAvailable || !this.bridge.openSettings) { + throw new Error('NFC settings cannot be opened - bridge not available'); + } + + try { + await this.bridge.openSettings(); + } catch (error) { + console.error('Error opening NFC settings through bridge:', error); + throw error; + } + } + + async startScanSession(options?: any): Promise { + if (!this.isBridgeAvailable) { + throw new Error('NFC scan cannot be started - bridge not available'); + } + + try { + await this.bridge.startNfcScan!(JSON.stringify(options || {})); + } catch (error) { + console.error('Error starting NFC scan through bridge:', error); + throw error; + } + } + + async stopScanSession(): Promise { + if (!this.isBridgeAvailable || !this.bridge.stopNfcScan) { + return; // Just silently return if bridge not available + } + + try { + await this.bridge.stopNfcScan!(); + } catch (error) { + console.error('Error stopping NFC scan through bridge:', error); + } + } + + async write(options: any): Promise { + if (!this.isBridgeAvailable || !this.bridge.writeNfcTag) { + throw new Error('NFC write is not supported - bridge not available'); + } + + try { + await this.bridge.writeNfcTag!(JSON.stringify(options)); + } catch (error) { + console.error('Error writing NFC tag through bridge:', error); + throw error; + } + } + + async makeReadOnly(): Promise { + throw new Error('makeReadOnly is not implemented in the iOS bridge'); + } + + async format(): Promise { + throw new Error('format is not implemented in the iOS bridge'); + } + + async erase(): Promise { + throw new Error('erase is not implemented in the iOS bridge'); + } + + async share(): Promise { + throw new Error('share is not supported in iOS'); + } + + async stopSharing(): Promise { + throw new Error('share is not supported in iOS'); + } + + 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 = {}; + } +} diff --git a/ios-detection.ts b/ios-detection.ts new file mode 100644 index 0000000..e5fa8d5 --- /dev/null +++ b/ios-detection.ts @@ -0,0 +1,73 @@ +// src/ios-detection.ts +export interface IosSupportInfo { + isIos: boolean; + version: number | null; + supportsNfc: boolean; + requiresNative: boolean; +} + +/** + * Provides detailed information about iOS NFC compatibility + */ +export class IosDetection { + /** + * Detects if the current device is running iOS and its NFC capabilities + */ + static getIosSupportInfo(): IosSupportInfo { + const userAgent = navigator.userAgent; + + // Detect iOS + const isIos = /iphone|ipad|ipod/i.test(userAgent); + if (!isIos) { + return { isIos: false, version: null, supportsNfc: false, requiresNative: false }; + } + + // Extract iOS version + const versionMatch = userAgent.match(/OS (\d+)_(\d+)_?(\d+)?/); + const version = versionMatch ? + parseInt(versionMatch[1], 10) + (parseInt(versionMatch[2], 10) / 10) : + null; + + // iOS NFC support info: + // - iOS 11+ supports NFC reading but requires a native app + // - No iOS version supports Web NFC API + const supportsNfc = version !== null && version >= 11; + + return { + isIos, + version, + supportsNfc, + requiresNative: true // iOS always requires native app for NFC + }; + } + + /** + * Checks if this device supports NFC in any form (native or web) + */ + static hasNfcHardware(): boolean { + const iosInfo = this.getIosSupportInfo(); + if (iosInfo.isIos) { + return iosInfo.supportsNfc; + } + + // For Android/other platforms, we check based on the user agent + return /android/i.test(navigator.userAgent); + } + + /** + * Provides guidance for enabling NFC on iOS + */ + static getIosNfcGuidance(): string { + const iosInfo = this.getIosSupportInfo(); + + if (!iosInfo.isIos) { + return ''; + } + + if (!iosInfo.supportsNfc) { + return 'Your iOS device does not support NFC or is running an iOS version below 11.0.'; + } + + return 'NFC on iOS requires a native app. The web browser cannot directly access NFC hardware on iOS devices.'; + } +} diff --git a/nfc.ts b/nfc.ts index c9fb72d..df5becd 100644 --- a/nfc.ts +++ b/nfc.ts @@ -10,6 +10,8 @@ import { NfcErrorType, } from "./definitions.js"; import { WebNfc } from "./web.js"; +import { IosBridgeNfc } from './ios-bridge.js'; +import { IosDetection } from './ios-detection.js'; /** * Main NFC class that provides access to NFC functionality. @@ -18,15 +20,24 @@ import { WebNfc } from "./web.js"; export class Nfc { private implementation: NfcPlugin; private listeners: Map> = new Map(); + private iosInfo: any; + constructor() { - // Currently we only have the Web implementation - this.implementation = new WebNfc(); + this.iosInfo = IosDetection.getIosSupportInfo(); + + if (this.iosInfo.isIos && typeof (window as any).nativeNfcBridge !== 'undefined') { + console.log('Using iOS Native Bridge for NFC'); + this.implementation = new IosBridgeNfc(); + } else { + this.implementation = new WebNfc(); + } // Set up status monitoring to track NFC availability this.monitorNfcStatus(); } + /** * Internal method to monitor NFC status changes */ diff --git a/web.ts b/web.ts index e37d094..5698db9 100644 --- a/web.ts +++ b/web.ts @@ -6,8 +6,10 @@ import { ShareOptions, PluginListenerHandle, TagDetectedEvent, + NfcErrorType, NdefRecord, -} from "./definitions.js"; +} from './definitions.js'; +import { IosDetection } from './ios-detection.js'; export class WebNfc implements NfcPlugin { private scanSessionActive = false; @@ -15,73 +17,104 @@ export class WebNfc implements NfcPlugin { private listeners: { [key: string]: Array<(...args: any[]) => void> } = {}; private ndefReader: any = null; private nfcSupported: boolean = false; + private iosInfo: any = null; 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; + // Check for iOS first + this.iosInfo = IosDetection.getIosSupportInfo(); + + if (this.iosInfo.isIos) { + this.nfcSupported = false; + console.info('iOS device detected:', this.iosInfo); + + if (this.iosInfo.supportsNfc) { + console.warn('iOS NFC requires a native app wrapper - web browser access is not supported'); + } else { + console.warn('NFC is not supported on this iOS device or iOS version'); + } + return; + } + + // Check Web NFC API for other platforms (primarily Android) + if (typeof window !== 'undefined') { + 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; + const isHttps = window.location.protocol === 'https:'; + const isPWA = window.matchMedia('(display-mode: standalone)').matches; - console.info("NFC Support Check:", { + 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), + 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"; + 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"); + console.warn('Web NFC not available: Not in browser environment'); } } async isEnabled(): Promise { + // For iOS, provide accurate info without throwing errors + if (this.iosInfo?.isIos) { + return { enabled: false }; + } + return { enabled: this.nfcSupported }; } async openSettings(): Promise { + // Special handling for iOS + if (this.iosInfo?.isIos) { + const message = IosDetection.getIosNfcGuidance(); + + // Use alert() on iOS to show guidance + if (typeof alert === 'function') { + alert(message); + } + + console.warn('iOS NFC guidance:', message); + return; + } + if (!this.nfcSupported) { throw this.createCompatibilityError(); } - console.warn( - "openSettings: On web, users must enable NFC in device settings manually." - ); + 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" - ); + alert('Please enable NFC in your device settings: Settings > Connected devices > Connection preferences > NFC'); } else { - alert("Please ensure NFC is enabled on your device."); + alert('Please ensure NFC is enabled on your device.'); } } + async startScanSession(options?: StartScanSessionOptions): Promise { + + if (this.iosInfo?.isIos) { + throw new Error("NFC reading is not supported in web browsers on iOS. " + + "To use NFC on iOS, you need a native app implementation."); + } + if (!this.nfcSupported) { throw this.createCompatibilityError(); } @@ -364,18 +397,10 @@ export class WebNfc implements NfcPlugin { } 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." - ); - } -} - -import { NFCDefinition } from "./definitions"; - -export class WebNFC { - // Implement your web NFC functionalities here - read(): NFCDefinition { - // Dummy implementation - return { id: "1", data: "sample data" }; + if (this.iosInfo?.isIos) { + return new Error("iOS browsers don't support the Web NFC API. To use NFC on iOS, you need a native app implementation."); + } + + 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.'); } }