Claude Skill

mobile-hardware-ble-nfc

BLE scanning/connecting/GATT operations with react-native-ble-plx, NFC tag reading/writing with react-native-nfc-manager, permissions, background mode, battery-efficient patterns

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_mobile-hardware-ble-nfc_skills_mobile-hardware-ble-nfc-3a51ef5.zip · 18 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/mobile-hardware-ble-nfc/skills/mobile-hardware-ble-nfc
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

BLE & NFC Patterns

Quick Guide: Use react-native-ble-plx for BLE (scanning, connecting, GATT read/write/monitor). Use react-native-nfc-manager for NFC (NDEF read/write, tag technology access). BLE values are Base64-encoded -- decode before use. NFC operations follow request-technology/operate/cancel-technology lifecycle. Always clean up: remove BLE subscriptions, call cancelTechnologyRequest() for NFC, and destroy() the BleManager. MTU defaults to 23 bytes (20 usable) -- negotiate higher on Android. iOS auto-negotiates up to 187 bytes.


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST call destroy() on BleManager when deallocating resources -- leaking the manager causes native memory leaks and zombie listeners)

(You MUST call discoverAllServicesAndCharacteristics() after connecting before any read/write/monitor operations -- GATT structure is not available until discovered)

(You MUST call cancelTechnologyRequest() in a finally block after every NFC operation -- failing to release the NFC session blocks subsequent scans)

(You MUST check BLE adapter state (PoweredOn) before scanning -- scanning while powered off or unauthorized throws errors silently on some devices)

(You MUST remove all BLE subscriptions (scan listeners, characteristic monitors, disconnect listeners) on cleanup -- leaked subscriptions cause crashes after component unmount)

</critical_requirements>


Auto-detection: react-native-ble-plx, BleManager, startDeviceScan, connectToDevice, monitorCharacteristicForDevice, writeCharacteristicWithResponseForDevice, readCharacteristicForDevice, requestMTUForDevice, react-native-nfc-manager, NfcManager, NfcTech, Ndef, requestTechnology, cancelTechnologyRequest, writeNdefMessage, ndefHandler, useCodeScanner BLE, BLE scanning, NFC tag, NDEF record, characteristic notification, GATT

When to use:

  • Scanning for and connecting to BLE peripherals (IoT sensors, wearables, medical devices)
  • Reading/writing BLE GATT characteristics and monitoring notifications
  • Reading NDEF tags or writing NDEF records to NFC tags
  • Implementing background BLE reconnection with state restoration
  • MTU negotiation for large data transfers over BLE
  • Accessing low-level NFC technologies (NfcA, IsoDep, MifareUltralight)

When NOT to use:

  • Classic Bluetooth audio/file transfer (different protocol, different libraries)
  • BLE peripheral/server mode (react-native-ble-plx is central-only)
  • Web-based Bluetooth (use Web Bluetooth API)
  • Wi-Fi Direct or peer-to-peer networking

Key patterns covered:

  • BLE lifecycle: scan, connect, discover services, read/write/monitor, disconnect
  • Battery-efficient scanning with UUID filters and scan modes
  • MTU negotiation (Android explicit, iOS automatic)
  • Characteristic subscriptions (notifications/indications) with cleanup
  • BLE reconnection and disconnect monitoring
  • NFC NDEF read/write lifecycle with technology request/cancel
  • NFC technology types and platform availability (iOS vs Android)
  • Permission handling for both BLE and NFC

Detailed Resources:

  • examples/core.md - BLE scanning, connecting, GATT operations, disconnect handling
  • examples/nfc.md - NFC NDEF reading/writing, technology types, platform differences
  • reference.md - API quick reference, permission matrix, decision frameworks



<decision_framework>

Decision Framework

BLE vs NFC

What kind of hardware interaction?
|
+-> Continuous connection with a peripheral (sensor, wearable)?
|   +-> BLE -- long-lived connection with GATT operations
|
+-> One-tap read/write (tag, card)?
|   +-> NFC -- session-based, tap and go
|
+-> Background monitoring of nearby devices?
|   +-> BLE -- background scanning with state restoration
|
+-> Quick device provisioning (write config to tag)?
    +-> NFC -- write NDEF record, tap target device

BLE Write Method

Which write method?
|
+-> Data MUST arrive reliably?
|   +-> writeCharacteristicWithResponseForService (acknowledged, slower)
|
+-> Speed matters more than reliability?
|   +-> writeCharacteristicWithoutResponseForService (fire-and-forget, faster)
|
+-> Large payload (> MTU)?
    +-> Negotiate higher MTU first, then use write-with-response

NFC Technology Selection

What kind of NFC tag?
|
+-> Standard NDEF content (URL, text, MIME)?
|   +-> NfcTech.Ndef (iOS + Android)
|
+-> ISO 14443-3A tag (raw commands)?
|   +-> NfcTech.NfcA (iOS + Android)
|
+-> Smart card / ISO 7816 (APDU commands)?
|   +-> NfcTech.IsoDep (iOS + Android)
|
+-> Mifare Classic (Android only)?
|   +-> NfcTech.MifareClassic
|
+-> Mifare Ultralight (Android only)?
    +-> NfcTech.MifareUltralight

</decision_framework>


<red_flags>

RED FLAGS

High Priority Issues:

  • Skipping discoverAllServicesAndCharacteristics() after connecting -- read/write/monitor calls fail silently or throw errors because GATT structure is not cached
  • Not removing BLE subscriptions on cleanup -- leaked subscriptions fire callbacks after component unmount, causing "setState on unmounted component" crashes
  • Forgetting cancelTechnologyRequest() in NFC finally block -- NFC session stays locked, all subsequent NFC operations fail until app restart
  • Creating multiple BleManager instances -- each instance allocates native resources. Create ONE and share it. Call destroy() only once on app teardown.
  • Scanning without UUID filter and without timeout -- scans all devices indefinitely, drains battery rapidly

Medium Priority Issues:

  • Not checking State.PoweredOn before scanning -- scanning while Bluetooth is off throws errors on some devices and does nothing on others
  • Not handling Base64 encoding/decoding for BLE characteristic values -- raw Base64 strings are not human-readable and cannot be compared directly
  • Using writeCharacteristicWithoutResponseForService for critical data -- fire-and-forget may lose data; use write-with-response for reliability
  • Not requesting MTU on Android -- default 23-byte MTU means 20 usable bytes per packet, making large transfers extremely slow
  • Hardcoding service/characteristic UUIDs inline instead of using named constants

Gotchas & Edge Cases:

  • iOS BLE device IDs are random UUIDs, not MAC addresses -- they can change after Bluetooth is toggled or the device restarts. Do not persist iOS device IDs for reconnection; use service UUID scanning instead.
  • Android 12+ changed BLE permissions -- BLUETOOTH_SCAN and BLUETOOTH_CONNECT replaced ACCESS_FINE_LOCATION for BLE scanning (set neverForLocation: true in Expo config if scanning does not need location)
  • Android 14+ defaults MTU to 517 bytes -- requestMTU may be unnecessary on newer Android devices. Check device.mtu after connection.
  • iOS auto-negotiates MTU up to 187 bytes -- calling requestMTU on iOS has no effect
  • onDeviceDisconnected fires once per registration -- you must re-register the listener after each reconnection if you want continued disconnect monitoring
  • NfcTech.NfcB, NfcF, NfcV are Android-only -- iOS has different equivalents (Iso15693IOS, FelicaIOS). Always check platform before requesting a technology.
  • NFC on iOS shows a system scan dialog -- you cannot customize it beyond the alertMessage. On Android, scanning is silent.
  • BLE allowDuplicates is iOS-only -- Android always emits duplicates. Deduplicate in your scan callback using a Set of device IDs.
  • HCE (Host Card Emulation) is NOT supported by react-native-nfc-manager -- use a dedicated library like react-native-hce for card emulation
  • NFC getTag() returns the last discovered tag -- if no tag was tapped during the session, it returns the previous tag. Always request technology first.

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md

(You MUST call destroy() on BleManager when deallocating resources -- leaking the manager causes native memory leaks and zombie listeners)

(You MUST call discoverAllServicesAndCharacteristics() after connecting before any read/write/monitor operations -- GATT structure is not available until discovered)

(You MUST call cancelTechnologyRequest() in a finally block after every NFC operation -- failing to release the NFC session blocks subsequent scans)

(You MUST check BLE adapter state (PoweredOn) before scanning -- scanning while powered off or unauthorized throws errors silently on some devices)

(You MUST remove all BLE subscriptions (scan listeners, characteristic monitors, disconnect listeners) on cleanup -- leaked subscriptions cause crashes after component unmount)

Failure to follow these rules will cause native crashes, memory leaks, blocked NFC sessions, and battery drain.

</critical_reminders>

Files (skills)
  • examples
    • core.md 14.4 KB
      # BLE & NFC - Core BLE Patterns
      
      > BLE scanning, connecting, GATT operations, characteristic monitoring, disconnect handling, and reconnection. See [nfc.md](nfc.md) for NFC patterns. See [SKILL.md](../SKILL.md) for red flags and decision guidance.
      
      **Prerequisites:** react-native-ble-plx v3.2+, base-64 (for encoding/decoding characteristic values)
      
      ---
      
      ## Pattern 1: BleManager Initialization and State Monitoring
      
      Create a single BleManager instance for the entire app. Monitor adapter state to know when BLE is ready.
      
      ```typescript
      import { BleManager, State, type Subscription } from "react-native-ble-plx";
      
      // Singleton -- create once, share everywhere
      const manager = new BleManager();
      
      function waitForPoweredOn(): Promise<void> {
        return new Promise((resolve, reject) => {
          const subscription = manager.onStateChange((state) => {
            if (state === State.PoweredOn) {
              subscription.remove();
              resolve();
            } else if (state === State.Unsupported) {
              subscription.remove();
              reject(new Error("BLE is not supported on this device"));
            } else if (state === State.Unauthorized) {
              subscription.remove();
              reject(new Error("Bluetooth permission not granted"));
            }
          }, true); // true = emit current state immediately
        });
      }
      
      // Usage
      async function initBle() {
        await waitForPoweredOn();
        // Now safe to scan and connect
      }
      
      // App teardown -- call once when app is being destroyed
      function teardownBle() {
        manager.destroy();
      }
      ```
      
      **Why good:** single manager instance avoids native resource leaks, state checked before operations, subscription removed after state resolved, `true` flag avoids missing initial state
      
      ```typescript
      // Bad: creating manager per component
      function MyComponent() {
        const manager = new BleManager(); // Leaks on every mount
        // ...
      }
      ```
      
      **Why bad:** each BleManager allocates native resources, unmounting without destroy() causes memory leaks
      
      ---
      
      ### Background Mode and State Restoration
      
      For apps that must maintain BLE connections when backgrounded (iOS):
      
      ```typescript
      const RESTORE_ID = "my-app-ble-restore";
      
      const manager = new BleManager({
        restoreStateIdentifier: RESTORE_ID,
        restoreStateFunction: (restoredState) => {
          if (restoredState?.connectedPeripherals) {
            // Re-establish monitoring on peripherals that were connected
            // when the app was terminated by iOS
            for (const device of restoredState.connectedPeripherals) {
              // Re-subscribe to characteristics
            }
          }
        },
      });
      ```
      
      **Platform setup:**
      
      - **iOS:** Enable "Uses Bluetooth LE Accessories" in Background Modes capability
      - **Android:** Set `isBackgroundEnabled: true` in Expo plugin config
      - **Expo:** Add `react-native-ble-plx` plugin with `{ isBackgroundEnabled: true, modes: ["central"] }` in app.json
      
      ---
      
      ## Pattern 2: BLE Scanning with Permissions
      
      Always request permissions before scanning. Filter by service UUID for battery efficiency.
      
      ```typescript
      import { Platform, PermissionsAndroid } from "react-native";
      import {
        BleManager,
        type Device,
        type Subscription,
        ScanMode,
      } from "react-native-ble-plx";
      
      const HEART_RATE_SERVICE = "0000180d-0000-1000-8000-00805f9b34fb";
      const SCAN_TIMEOUT_MS = 15000;
      const ANDROID_12_API_LEVEL = 31;
      
      async function requestBlePermissions(): Promise<boolean> {
        if (Platform.OS === "android") {
          if (Platform.Version >= ANDROID_12_API_LEVEL) {
            const results = await PermissionsAndroid.requestMultiple([
              PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
              PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
            ]);
            return Object.values(results).every(
              (status) => status === PermissionsAndroid.RESULTS.GRANTED,
            );
          }
          // Android < 12: location permission required for BLE scanning
          const result = await PermissionsAndroid.request(
            PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
          );
          return result === PermissionsAndroid.RESULTS.GRANTED;
        }
        // iOS: handled via Info.plist (NSBluetoothAlwaysUsageDescription)
        return true;
      }
      
      async function scanForDevices(
        manager: BleManager,
        onDeviceFound: (device: Device) => void,
      ): Promise<void> {
        const hasPermission = await requestBlePermissions();
        if (!hasPermission) {
          throw new Error("BLE permissions not granted");
        }
      
        await waitForPoweredOn();
      
        const seenDeviceIds = new Set<string>();
      
        manager.startDeviceScan(
          [HEART_RATE_SERVICE], // Filter by service UUID -- null scans ALL devices (battery drain)
          {
            allowDuplicates: false, // iOS only -- Android always emits duplicates
            // Android scan modes:
            // ScanMode.LowLatency -- fastest, highest battery use
            // ScanMode.Balanced -- default
            // ScanMode.LowPower -- slowest, lowest battery
            // ScanMode.Opportunistic -- piggybacks on other apps' scans
          },
          (error, device) => {
            if (error) {
              console.error("Scan error:", error.message);
              return;
            }
            if (!device) return;
      
            // Manual deduplication for Android (allowDuplicates has no effect)
            if (seenDeviceIds.has(device.id)) return;
            seenDeviceIds.add(device.id);
      
            onDeviceFound(device);
          },
        );
      
        // Always set a timeout to stop scanning
        setTimeout(() => {
          manager.stopDeviceScan();
        }, SCAN_TIMEOUT_MS);
      }
      ```
      
      **Why good:** permissions checked per Android API level, UUID filter reduces battery drain, scan timeout prevents indefinite scanning, manual deduplication handles Android behavior, named constants for UUIDs and timeouts
      
      ```typescript
      // Bad: scanning without filter, no timeout, no permissions
      manager.startDeviceScan(null, null, (error, device) => {
        setDevices((prev) => [...prev, device!]);
      });
      ```
      
      **Why bad:** null UUID filter scans all nearby BLE devices (battery killer), no timeout means scan runs forever, no permission check, no null guard on device, setState on every callback floods renders
      
      ---
      
      ## Pattern 3: Connection, GATT Discovery, and MTU Negotiation
      
      The full connection lifecycle: connect -> request MTU -> discover GATT -> operate.
      
      ```typescript
      import { Platform } from "react-native";
      import { type Device, type Subscription } from "react-native-ble-plx";
      
      const DESIRED_MTU = 512;
      const CONNECTION_TIMEOUT_MS = 10000;
      const BLE_ATT_HEADER_BYTES = 3;
      
      async function connectToDevice(
        manager: BleManager,
        deviceId: string,
      ): Promise<{ device: Device; mtu: number }> {
        // Connect with options
        const device = await manager.connectToDevice(deviceId, {
          requestMTU: Platform.OS === "android" ? DESIRED_MTU : undefined,
          timeout: CONNECTION_TIMEOUT_MS,
          // autoConnect: false -- default. true = connect when device becomes available (slower)
        });
      
        // CRITICAL: Discover GATT structure before any operations
        await device.discoverAllServicesAndCharacteristics();
      
        // Calculate usable payload size
        const usableBytes = device.mtu - BLE_ATT_HEADER_BYTES;
      
        return { device, mtu: usableBytes };
      }
      ```
      
      **Why good:** MTU requested only on Android (iOS auto-negotiates), connection timeout prevents hanging, GATT discovered before operations, usable bytes calculated with ATT header subtracted
      
      ```typescript
      // Bad: reading immediately after connect
      const device = await manager.connectToDevice(deviceId);
      const char = await device.readCharacteristicForService(svc, chr);
      // Throws: services not discovered
      ```
      
      **Why bad:** GATT structure not discovered, read/write/monitor operations fail because the service/characteristic tree is not cached locally
      
      ---
      
      ## Pattern 4: Reading and Writing Characteristics
      
      All BLE values are Base64-encoded. Use a library like `base-64` for encoding/decoding.
      
      ```typescript
      import { decode as atob, encode as btoa } from "base-64";
      import type { Characteristic } from "react-native-ble-plx";
      
      const DEVICE_INFO_SERVICE = "0000180a-0000-1000-8000-00805f9b34fb";
      const FIRMWARE_REV_CHAR = "00002a26-0000-1000-8000-00805f9b34fb";
      const CONTROL_POINT_CHAR = "00002a55-0000-1000-8000-00805f9b34fb";
      
      // Read a characteristic
      async function readFirmwareVersion(device: Device): Promise<string> {
        const characteristic = await device.readCharacteristicForService(
          DEVICE_INFO_SERVICE,
          FIRMWARE_REV_CHAR,
        );
        return atob(characteristic.value ?? "");
      }
      
      // Write with response (acknowledged -- reliable)
      async function sendCommand(device: Device, command: string): Promise<void> {
        const base64Value = btoa(command);
        await device.writeCharacteristicWithResponseForService(
          DEVICE_INFO_SERVICE,
          CONTROL_POINT_CHAR,
          base64Value,
        );
      }
      
      // Write without response (fire-and-forget -- faster)
      async function sendFastCommand(device: Device, command: string): Promise<void> {
        const base64Value = btoa(command);
        await device.writeCharacteristicWithoutResponseForService(
          DEVICE_INFO_SERVICE,
          CONTROL_POINT_CHAR,
          base64Value,
        );
      }
      ```
      
      **Why good:** Base64 encode/decode explicit, named constants for all UUIDs, write-with-response for reliable delivery, write-without-response option for speed, value null-coalesced before decode
      
      ---
      
      ## Pattern 5: Characteristic Monitoring (Notifications/Indications)
      
      Subscribe to real-time value changes from a characteristic. Returns a Subscription that MUST be removed.
      
      ```typescript
      import { useEffect, useRef, useState, useCallback } from "react";
      import { decode as atob } from "base-64";
      import type { Device, Subscription } from "react-native-ble-plx";
      
      const HEART_RATE_SERVICE = "0000180d-0000-1000-8000-00805f9b34fb";
      const HEART_RATE_MEASUREMENT = "00002a37-0000-1000-8000-00805f9b34fb";
      
      export function useHeartRateMonitor(device: Device | null) {
        const [heartRate, setHeartRate] = useState<number | null>(null);
        const subscriptionRef = useRef<Subscription | null>(null);
      
        useEffect(() => {
          if (!device) return;
      
          subscriptionRef.current = device.monitorCharacteristicForService(
            HEART_RATE_SERVICE,
            HEART_RATE_MEASUREMENT,
            (error, characteristic) => {
              if (error) {
                console.error("Monitor error:", error.message);
                return;
              }
              if (!characteristic?.value) return;
      
              // Heart rate measurement format: first byte is flags, second byte is HR value
              const raw = atob(characteristic.value);
              const heartRateValue = raw.charCodeAt(1);
              setHeartRate(heartRateValue);
            },
          );
      
          // CRITICAL: Remove subscription on cleanup
          return () => {
            subscriptionRef.current?.remove();
            subscriptionRef.current = null;
          };
        }, [device]);
      
        return heartRate;
      }
      ```
      
      **Why good:** subscription stored in ref and removed in cleanup, null checks on device and characteristic, error handled in callback, useEffect cleanup prevents leaked listeners
      
      ```typescript
      // Bad: no cleanup
      device.monitorCharacteristicForService(svc, chr, (err, char) => {
        setHeartRate(parseHR(char));
      });
      // Subscription leaked -- fires after unmount, causes crash
      ```
      
      **Why bad:** subscription not stored or removed, callback fires after component unmount causing "setState on unmounted component" crash
      
      ---
      
      ## Pattern 6: Disconnect Monitoring and Reconnection
      
      Monitor for unexpected disconnects and implement reconnection with backoff.
      
      ```typescript
      import type { BleManager, Device, Subscription } from "react-native-ble-plx";
      
      const MAX_RECONNECT_ATTEMPTS = 5;
      const BASE_DELAY_MS = 1000;
      const MAX_DELAY_MS = 30000;
      const BACKOFF_MULTIPLIER = 2;
      
      async function connectWithReconnection(
        manager: BleManager,
        deviceId: string,
        onConnected: (device: Device) => void,
        onDisconnected: () => void,
      ): Promise<() => void> {
        let disconnectSubscription: Subscription | null = null;
        let isCleanedUp = false;
        let reconnectAttempts = 0;
      
        async function connect() {
          try {
            const device = await manager.connectToDevice(deviceId);
            await device.discoverAllServicesAndCharacteristics();
            reconnectAttempts = 0; // Reset on successful connection
      
            onConnected(device);
      
            // Monitor for disconnects -- fires once per registration
            disconnectSubscription = device.onDisconnected((error) => {
              if (isCleanedUp) return;
              onDisconnected();
              attemptReconnect();
            });
          } catch (error) {
            attemptReconnect();
          }
        }
      
        async function attemptReconnect() {
          if (isCleanedUp || reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return;
      
          reconnectAttempts++;
          const delay = Math.min(
            BASE_DELAY_MS * Math.pow(BACKOFF_MULTIPLIER, reconnectAttempts - 1),
            MAX_DELAY_MS,
          );
      
          await new Promise((resolve) => setTimeout(resolve, delay));
          if (!isCleanedUp) {
            await connect();
          }
        }
      
        await connect();
      
        // Return cleanup function
        return () => {
          isCleanedUp = true;
          disconnectSubscription?.remove();
          manager.cancelDeviceConnection(deviceId).catch(() => {
            // Ignore disconnect errors during cleanup
          });
        };
      }
      ```
      
      **Why good:** exponential backoff prevents aggressive reconnection, max attempts prevents infinite loops, cleanup function cancels connection and removes listeners, `onDisconnected` fires once per registration (re-registered after each reconnect), named constants for all timing values
      
      ---
      
      ## Pattern 7: Listing Services and Characteristics
      
      After GATT discovery, enumerate available services and their characteristics.
      
      ```typescript
      import type { Device, Service, Characteristic } from "react-native-ble-plx";
      
      interface GattMap {
        services: Array<{
          uuid: string;
          characteristics: Array<{
            uuid: string;
            isReadable: boolean;
            isWritableWithResponse: boolean;
            isWritableWithoutResponse: boolean;
            isNotifiable: boolean;
            isIndicatable: boolean;
          }>;
        }>;
      }
      
      async function discoverGattStructure(device: Device): Promise<GattMap> {
        const services = await device.services();
      
        const serviceEntries = await Promise.all(
          services.map(async (service) => {
            const characteristics = await service.characteristics();
            return {
              uuid: service.uuid,
              characteristics: characteristics.map((char) => ({
                uuid: char.uuid,
                isReadable: char.isReadable,
                isWritableWithResponse: char.isWritableWithResponse,
                isWritableWithoutResponse: char.isWritableWithoutResponse,
                isNotifiable: char.isNotifiable,
                isIndicatable: char.isIndicatable,
              })),
            };
          }),
        );
      
        return { services: serviceEntries };
      }
      ```
      
      **Why good:** enumerates all services and characteristics with capability flags, useful for debugging unknown peripherals, Promise.all parallelizes service enumeration
      
      **Key:** Always check `isNotifiable` / `isIndicatable` before calling `monitorCharacteristicForService` -- not all characteristics support notifications.
      
    • nfc.md 11.4 KB
      # BLE & NFC - NFC Patterns
      
      > NFC NDEF reading/writing, technology types, platform differences, and low-level tag access. See [core.md](core.md) for BLE patterns. See [SKILL.md](../SKILL.md) for red flags and decision guidance.
      
      **Prerequisites:** react-native-nfc-manager v3.14+
      
      ---
      
      ## Pattern 1: NFC Initialization and Capability Check
      
      Initialize NfcManager once at app start. Check if NFC is supported and enabled.
      
      ```typescript
      import NfcManager from "react-native-nfc-manager";
      
      async function initNfc(): Promise<boolean> {
        const isSupported = await NfcManager.isSupported();
        if (!isSupported) {
          return false;
        }
      
        await NfcManager.start();
        return true;
      }
      
      // Check if NFC is currently enabled (user may have disabled it in settings)
      async function isNfcEnabled(): Promise<boolean> {
        try {
          return await NfcManager.isEnabled();
        } catch {
          return false;
        }
      }
      ```
      
      **Why good:** support checked before start, start() called once, isEnabled() checks runtime state (user may toggle NFC in settings)
      
      **Platform setup:**
      
      - **iOS:** Add `NFCReaderUsageDescription` to Info.plist, enable "Near Field Communication Tag Reading" capability in Xcode
      - **Android:** Add `<uses-permission android:name="android.permission.NFC" />` to AndroidManifest.xml
      - **Android 12+:** Set `compileSdkVersion` to 31+ (PendingIntent mutability requirement)
      
      ---
      
      ## Pattern 2: Reading NDEF Tags
      
      Request NDEF technology, read the tag, parse records. Always cancel in finally.
      
      ```typescript
      import NfcManager, { NfcTech, Ndef } from "react-native-nfc-manager";
      
      interface NdefRecord {
        tnf: number; // Type Name Format
        type: number[];
        id: number[];
        payload: number[];
      }
      
      interface ParsedTag {
        id: string | null;
        records: Array<{
          type: string;
          value: string;
        }>;
      }
      
      async function readNdefTag(): Promise<ParsedTag | null> {
        try {
          await NfcManager.requestTechnology(NfcTech.Ndef);
          const tag = await NfcManager.getTag();
      
          if (!tag?.ndefMessage || tag.ndefMessage.length === 0) {
            return { id: tag?.id ?? null, records: [] };
          }
      
          const records = tag.ndefMessage.map((record: NdefRecord) => {
            const type = String.fromCharCode(...record.type);
      
            // Decode based on record type
            if (type === "U") {
              // URI record -- first payload byte is URI prefix code
              return {
                type: "uri",
                value: Ndef.uri.decodePayload(
                  record.payload as unknown as Uint8Array,
                ),
              };
            }
            if (type === "T") {
              // Text record -- first bytes encode language
              return {
                type: "text",
                value: Ndef.text.decodePayload(
                  record.payload as unknown as Uint8Array,
                ),
              };
            }
      
            // Raw payload fallback
            return {
              type: "unknown",
              value: String.fromCharCode(...record.payload),
            };
          });
      
          return { id: tag.id ?? null, records };
        } catch (error) {
          // NfcError: user cancelled, tag removed too soon, or technology unavailable
          return null;
        } finally {
          // CRITICAL: Always release the NFC session
          await NfcManager.cancelTechnologyRequest();
        }
      }
      ```
      
      **Why good:** try/finally ensures cancelTechnologyRequest is always called, record type checked for proper decoding (URI vs text vs raw), tag ID extracted, null checks on tag and ndefMessage
      
      ```typescript
      // Bad: no finally, no error handling
      async function readTag() {
        await NfcManager.requestTechnology(NfcTech.Ndef);
        const tag = await NfcManager.getTag();
        return tag; // Technology never released if error thrown above
      }
      ```
      
      **Why bad:** if getTag() throws (tag removed too soon), technology is never released, blocking all subsequent NFC operations until app restart
      
      ---
      
      ## Pattern 3: Writing NDEF Tags
      
      Write URL records, text records, or multi-record messages.
      
      ```typescript
      import NfcManager, { NfcTech, Ndef } from "react-native-nfc-manager";
      
      // Write a single URI record
      async function writeUrlTag(url: string): Promise<boolean> {
        try {
          await NfcManager.requestTechnology(NfcTech.Ndef);
      
          const bytes = Ndef.encodeMessage([Ndef.uriRecord(url)]);
          await NfcManager.ndefHandler.writeNdefMessage(bytes);
          return true;
        } catch {
          return false;
        } finally {
          await NfcManager.cancelTechnologyRequest();
        }
      }
      
      // Write a single text record
      async function writeTextTag(text: string): Promise<boolean> {
        try {
          await NfcManager.requestTechnology(NfcTech.Ndef);
      
          const bytes = Ndef.encodeMessage([Ndef.textRecord(text)]);
          await NfcManager.ndefHandler.writeNdefMessage(bytes);
          return true;
        } catch {
          return false;
        } finally {
          await NfcManager.cancelTechnologyRequest();
        }
      }
      
      // Write multiple records in one NDEF message
      async function writeMultiRecordTag(
        url: string,
        description: string,
      ): Promise<boolean> {
        try {
          await NfcManager.requestTechnology(NfcTech.Ndef);
      
          const bytes = Ndef.encodeMessage([
            Ndef.uriRecord(url),
            Ndef.textRecord(description),
          ]);
          await NfcManager.ndefHandler.writeNdefMessage(bytes);
          return true;
        } catch {
          return false;
        } finally {
          await NfcManager.cancelTechnologyRequest();
        }
      }
      ```
      
      **Why good:** Ndef utilities used for proper record encoding, try/finally in every function, multi-record message shows composability, boolean return signals success/failure
      
      ---
      
      ## Pattern 4: NFC Technology Types and Platform Availability
      
      Different NFC technologies have different platform support. Always check platform before requesting a technology.
      
      ```typescript
      import { Platform } from "react-native";
      import NfcManager, { NfcTech } from "react-native-nfc-manager";
      
      // Technology availability matrix
      // | Technology          | Android | iOS |
      // |---------------------|---------|-----|
      // | NfcTech.Ndef        |    Y    |  Y  |
      // | NfcTech.NfcA        |    Y    |  Y  |
      // | NfcTech.IsoDep      |    Y    |  Y  |
      // | NfcTech.NfcB        |    Y    |  N  |
      // | NfcTech.NfcF        |    Y    |  N  |
      // | NfcTech.NfcV        |    Y    |  N  |
      // | NfcTech.MifareClassic     | Y |  N  |
      // | NfcTech.MifareUltralight  | Y |  N  |
      // | NfcTech.MifareIOS         | N |  Y  |
      // | NfcTech.Iso15693IOS       | N |  Y  |
      // | NfcTech.FelicaIOS         | N |  Y  |
      
      // Safe technology request with platform check
      async function requestTechnologySafe(tech: NfcTech): Promise<boolean> {
        const androidOnly: NfcTech[] = [
          NfcTech.NfcB,
          NfcTech.NfcF,
          NfcTech.NfcV,
          NfcTech.MifareClassic,
          NfcTech.MifareUltralight,
        ];
      
        const iosOnly: NfcTech[] = [
          NfcTech.MifareIOS,
          NfcTech.Iso15693IOS,
          NfcTech.FelicaIOS,
        ];
      
        if (Platform.OS === "ios" && androidOnly.includes(tech)) {
          return false; // Not available on iOS
        }
        if (Platform.OS === "android" && iosOnly.includes(tech)) {
          return false; // Not available on Android
        }
      
        try {
          await NfcManager.requestTechnology(tech);
          return true;
        } catch {
          return false;
        }
      }
      ```
      
      **Why good:** platform-specific technologies listed explicitly, platform checked before requesting, error handled for unsupported technologies
      
      ---
      
      ## Pattern 5: Low-Level Tag Access (NfcA / IsoDep)
      
      For tags that don't use NDEF, access raw tag commands via technology-specific handlers.
      
      ```typescript
      import NfcManager, { NfcTech } from "react-native-nfc-manager";
      
      // Read Mifare Ultralight pages (Android only)
      const PAGES_PER_READ = 4; // Mifare Ultralight reads 4 pages (16 bytes) at a time
      
      async function readMifareUltralight(): Promise<number[][] | null> {
        if (Platform.OS !== "android") return null;
      
        try {
          await NfcManager.requestTechnology(NfcTech.MifareUltralight);
      
          const pages: number[][] = [];
          const PAGE_COUNT = 16; // Total pages to read
      
          for (let i = 0; i < PAGE_COUNT; i += PAGES_PER_READ) {
            const pageData =
              await NfcManager.mifareUltralightHandlerAndroid.mifareUltralightReadPages(
                i,
              );
            pages.push(pageData);
          }
      
          return pages;
        } catch {
          return null;
        } finally {
          await NfcManager.cancelTechnologyRequest();
        }
      }
      
      // Send APDU command via IsoDep (smart card interaction)
      async function sendApduCommand(command: number[]): Promise<number[] | null> {
        try {
          await NfcManager.requestTechnology(NfcTech.IsoDep);
          const response = await NfcManager.isoDepHandler.transceive(command);
          return response;
        } catch {
          return null;
        } finally {
          await NfcManager.cancelTechnologyRequest();
        }
      }
      ```
      
      **Why good:** platform checked for Android-only technology, try/finally for cleanup, named constants for page count, handler accessed through the correct technology-specific property
      
      ---
      
      ## Pattern 6: iOS NFC Alert Message
      
      iOS shows a system NFC scanning dialog. Customize the alert message.
      
      ```typescript
      import { Platform } from "react-native";
      import NfcManager, { NfcTech } from "react-native-nfc-manager";
      
      async function readTagWithCustomAlert(): Promise<void> {
        try {
          if (Platform.OS === "ios") {
            // iOS: requestTechnology accepts an options object with alertMessage
            await NfcManager.requestTechnology(NfcTech.Ndef, {
              alertMessage: "Hold your device near the NFC tag",
            });
          } else {
            // Android: scanning is silent (no system dialog)
            await NfcManager.requestTechnology(NfcTech.Ndef);
          }
      
          const tag = await NfcManager.getTag();
          // Process tag...
      
          if (Platform.OS === "ios") {
            // iOS: dismiss the system dialog with a success message
            await NfcManager.setAlertMessageIOS("Tag read successfully!");
          }
        } catch {
          // User cancelled or tag not found
        } finally {
          await NfcManager.cancelTechnologyRequest();
        }
      }
      ```
      
      **Why good:** platform check for iOS-specific alert, custom message improves UX, success message provides feedback before dialog dismisses, Android path stays simple
      
      ---
      
      ## Pattern 7: NFC with React Hooks
      
      Wrap NFC operations in a hook for component-level use.
      
      ```typescript
      import { useState, useCallback, useEffect } from "react";
      import NfcManager, { NfcTech, Ndef } from "react-native-nfc-manager";
      
      interface UseNfcReaderResult {
        isScanning: boolean;
        lastTag: string | null;
        error: string | null;
        startScan: () => Promise<void>;
        cancelScan: () => Promise<void>;
      }
      
      export function useNfcReader(): UseNfcReaderResult {
        const [isScanning, setIsScanning] = useState(false);
        const [lastTag, setLastTag] = useState<string | null>(null);
        const [error, setError] = useState<string | null>(null);
      
        // Initialize NFC on mount
        useEffect(() => {
          NfcManager.start().catch(() => {
            setError("NFC not supported");
          });
        }, []);
      
        const startScan = useCallback(async () => {
          setIsScanning(true);
          setError(null);
      
          try {
            await NfcManager.requestTechnology(NfcTech.Ndef);
            const tag = await NfcManager.getTag();
      
            if (tag?.ndefMessage?.[0]) {
              const payload = tag.ndefMessage[0].payload;
              const text = Ndef.text.decodePayload(payload as unknown as Uint8Array);
              setLastTag(text);
            } else {
              setLastTag(null);
            }
          } catch {
            setError("Scan cancelled or failed");
          } finally {
            await NfcManager.cancelTechnologyRequest();
            setIsScanning(false);
          }
        }, []);
      
        const cancelScan = useCallback(async () => {
          await NfcManager.cancelTechnologyRequest();
          setIsScanning(false);
        }, []);
      
        return { isScanning, lastTag, error, startScan, cancelScan };
      }
      ```
      
      **Why good:** hook encapsulates NFC lifecycle, isScanning tracks UI state, cleanup in finally, cancel exposed for user-initiated abort, error state for UI feedback, NfcManager.start() called once on mount
      
  • reference.md 11.9 KB
    # BLE & NFC Quick Reference
    
    > API quick reference, permission matrix, and decision frameworks. See [SKILL.md](SKILL.md) for red flags and anti-patterns.
    
    ---
    
    ## BLE Connection Flow
    
    ```
    1. new BleManager()               -- create once, share globally
    2. onStateChange(PoweredOn)       -- wait for adapter ready
    3. startDeviceScan(UUIDs, opts)   -- scan with UUID filter
    4. stopDeviceScan()               -- stop when target found or timeout
    5. connectToDevice(id, opts)      -- establish connection (requestMTU on Android)
    6. discoverAllServicesAndCharacteristics() -- MUST call before read/write/monitor
    7. read / write / monitor         -- GATT operations
    8. cancelConnection()             -- disconnect
    9. destroy()                      -- cleanup manager on app teardown
    ```
    
    ---
    
    ## BLE Permission Matrix
    
    | Platform                 | API Level | Required Permissions                               | Notes                                         |
    | ------------------------ | --------- | -------------------------------------------------- | --------------------------------------------- |
    | **iOS**                  | All       | Info.plist: `NSBluetoothAlwaysUsageDescription`    | Handled at build time, no runtime request     |
    | **Android 12+**          | 31+       | `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT`              | Runtime request via PermissionsAndroid        |
    | **Android 10-11**        | 29-30     | `ACCESS_FINE_LOCATION`                             | Location required for BLE scanning            |
    | **Android <10**          | 23-28     | `ACCESS_COARSE_LOCATION` or `ACCESS_FINE_LOCATION` | Either works                                  |
    | **Android (background)** | 29+       | `ACCESS_BACKGROUND_LOCATION`                       | Additional permission for background scanning |
    
    **Expo config plugin permissions:**
    
    ```json
    {
      "plugins": [
        [
          "react-native-ble-plx",
          {
            "isBackgroundEnabled": true,
            "modes": ["central"],
            "neverForLocation": true,
            "bluetoothAlwaysPermission": "This app uses Bluetooth to communicate with your device"
          }
        ]
      ]
    }
    ```
    
    - `neverForLocation: true` -- declares scanning does not use location (Android 12+, avoids location permission)
    - `modes: ["peripheral", "central"]` -- iOS background modes
    
    ---
    
    ## NFC Permission Matrix
    
    | Platform          | Required Setup                                                                 | Notes                                |
    | ----------------- | ------------------------------------------------------------------------------ | ------------------------------------ |
    | **iOS**           | Info.plist: `NFCReaderUsageDescription`                                        | Required                             |
    | **iOS**           | Xcode: "Near Field Communication Tag Reading" capability                       | Required                             |
    | **iOS (ISO7816)** | Info.plist: `com.apple.developer.nfc.readersession.iso7816.select-identifiers` | For smart card AIDs                  |
    | **Android**       | Manifest: `<uses-permission android:name="android.permission.NFC" />`          | Required                             |
    | **Android 12+**   | `compileSdkVersion >= 31`                                                      | PendingIntent mutability requirement |
    
    ---
    
    ## BLE Adapter States
    
    | State          | Meaning            | Action                                 |
    | -------------- | ------------------ | -------------------------------------- |
    | `Unknown`      | Transitioning      | Wait for next state                    |
    | `Resetting`    | Resetting          | Wait for PoweredOn                     |
    | `Unsupported`  | No BLE hardware    | Show error, disable BLE features       |
    | `Unauthorized` | Permission denied  | Request permission or link to Settings |
    | `PoweredOff`   | Bluetooth disabled | Prompt user to enable Bluetooth        |
    | `PoweredOn`    | Ready              | Safe to scan and connect               |
    
    ---
    
    ## BLE Scan Modes (Android)
    
    | Mode                     | Battery Impact | Scan Interval             | Use Case                       |
    | ------------------------ | -------------- | ------------------------- | ------------------------------ |
    | `ScanMode.LowLatency`    | High           | Continuous                | Finding a specific device fast |
    | `ScanMode.Balanced`      | Medium         | ~5s on / ~5s off          | Default, general scanning      |
    | `ScanMode.LowPower`      | Low            | ~0.5s on / ~4.5s off      | Background monitoring          |
    | `ScanMode.Opportunistic` | Minimal        | Piggybacks on other scans | Passive discovery              |
    
    ---
    
    ## BLE MTU Reference
    
    | Platform        | Default MTU | Auto-Negotiated           | Max MTU                                   |
    | --------------- | ----------- | ------------------------- | ----------------------------------------- |
    | **iOS**         | 23          | Yes, up to 187            | 187 (negotiated automatically on connect) |
    | **Android <14** | 23          | No (must call requestMTU) | 517                                       |
    | **Android 14+** | 517         | Yes                       | 517                                       |
    
    **Usable payload = MTU - 3 bytes** (ATT protocol header)
    
    | MTU              | Usable Bytes | Packets for 100 bytes |
    | ---------------- | ------------ | --------------------- |
    | 23 (default)     | 20           | 5                     |
    | 187 (iOS auto)   | 184          | 1                     |
    | 512 (negotiated) | 509          | 1                     |
    
    ---
    
    ## NFC Technology Handlers
    
    | Technology                 | Handler Property                 | Platform      | Methods                                                                                   |
    | -------------------------- | -------------------------------- | ------------- | ----------------------------------------------------------------------------------------- |
    | `NfcTech.Ndef`             | `ndefHandler`                    | iOS + Android | `writeNdefMessage()`, `makeReadOnly()`, `getNdefStatus()`                                 |
    | `NfcTech.NfcA`             | `nfcAHandler`                    | iOS + Android | `transceive()`                                                                            |
    | `NfcTech.IsoDep`           | `isoDepHandler`                  | iOS + Android | `transceive()`                                                                            |
    | `NfcTech.NfcV`             | `nfcVHandler`                    | Android       | `transceive()`                                                                            |
    | `NfcTech.MifareClassic`    | `mifareClassicHandlerAndroid`    | Android       | `mifareClassicAuthenticateA/B()`, `mifareClassicReadBlock()`, `mifareClassicWriteBlock()` |
    | `NfcTech.MifareUltralight` | `mifareUltralightHandlerAndroid` | Android       | `mifareUltralightReadPages()`, `mifareUltralightWritePage()`                              |
    
    ---
    
    ## Ndef Utility Methods
    
    | Method                             | Purpose                        | Example                                     |
    | ---------------------------------- | ------------------------------ | ------------------------------------------- |
    | `Ndef.uriRecord(uri)`              | Create URI record              | `Ndef.uriRecord("https://example.com")`     |
    | `Ndef.textRecord(text, lang?)`     | Create text record             | `Ndef.textRecord("Hello", "en")`            |
    | `Ndef.encodeMessage(records)`      | Encode record array to bytes   | `Ndef.encodeMessage([Ndef.uriRecord(url)])` |
    | `Ndef.uri.decodePayload(payload)`  | Decode URI from payload bytes  | Returns decoded URL string                  |
    | `Ndef.text.decodePayload(payload)` | Decode text from payload bytes | Returns decoded text string                 |
    | `Ndef.uri.encodePayload(uri)`      | Encode URI to payload bytes    | Returns byte array                          |
    | `Ndef.text.encodePayload(text)`    | Encode text to payload bytes   | Returns byte array                          |
    
    ---
    
    ## BLE Error Codes
    
    | Category       | Error Codes                                                            | Meaning               |
    | -------------- | ---------------------------------------------------------------------- | --------------------- |
    | **Adapter**    | `BluetoothUnsupported`, `BluetoothUnauthorized`, `BluetoothPoweredOff` | BLE not available     |
    | **Scanning**   | `BluetoothScanStartFailed`                                             | Scan could not start  |
    | **Connection** | `DeviceNotFound`, `DeviceNotConnected`, `DeviceConnectionFailed`       | Connection issues     |
    | **Discovery**  | `ServiceNotFound`, `CharacteristicNotFound`, `DescriptorNotFound`      | GATT element missing  |
    | **Operations** | `CharacteristicReadFailed`, `CharacteristicWriteFailed`                | Read/write failure    |
    | **Control**    | `OperationCancelled`, `OperationTimedOut`                              | Operation interrupted |
    
    ---
    
    ## BLE Characteristic Capabilities
    
    Before operating on a characteristic, check its capability flags:
    
    | Flag                        | Meaning                                            | Required For                                     |
    | --------------------------- | -------------------------------------------------- | ------------------------------------------------ |
    | `isReadable`                | Value can be read                                  | `readCharacteristicForService()`                 |
    | `isWritableWithResponse`    | Write-acknowledged supported                       | `writeCharacteristicWithResponseForService()`    |
    | `isWritableWithoutResponse` | Fire-and-forget write                              | `writeCharacteristicWithoutResponseForService()` |
    | `isNotifiable`              | Notifications supported                            | `monitorCharacteristicForService()`              |
    | `isIndicatable`             | Indications supported (acknowledged notifications) | `monitorCharacteristicForService()`              |
    
    ---
    
    ## Common BLE Service UUIDs
    
    | Service            | UUID     | Purpose                        |
    | ------------------ | -------- | ------------------------------ |
    | Generic Access     | `0x1800` | Device name, appearance        |
    | Generic Attribute  | `0x1801` | Service changed indication     |
    | Device Information | `0x180A` | Manufacturer, firmware, serial |
    | Battery Service    | `0x180F` | Battery level                  |
    | Heart Rate         | `0x180D` | Heart rate measurement         |
    | Health Thermometer | `0x1809` | Temperature measurement        |
    | Blood Pressure     | `0x1810` | Blood pressure measurement     |
    | Current Time       | `0x1805` | Current time                   |
    
    **Note:** Full UUIDs follow the format `0000XXXX-0000-1000-8000-00805f9b34fb` where `XXXX` is the short UUID.
    
    ---
    
    ## BLE Connection Options
    
    | Option        | Type          | Default | Notes                                                  |
    | ------------- | ------------- | ------- | ------------------------------------------------------ |
    | `requestMTU`  | number        | -       | Android only, request MTU during connection            |
    | `timeout`     | number        | -       | Connection timeout in ms                               |
    | `autoConnect` | boolean       | false   | Connect when device available (slower initial connect) |
    | `refreshGatt` | "OnConnected" | -       | Android only, refresh GATT cache on connect            |
    
    ---
    
    ## Battery-Efficient BLE Checklist
    
    - [ ] Scanning with UUID filter (not `null`) to reduce radio activity
    - [ ] Scan timeout set (never scan indefinitely)
    - [ ] Scan stopped as soon as target device found
    - [ ] Using `ScanMode.Balanced` or `ScanMode.LowPower` on Android (not `LowLatency` unless needed)
    - [ ] Characteristic monitoring subscriptions removed when no longer needed
    - [ ] Disconnect from device when communication is complete
    - [ ] Background scanning uses `ScanMode.LowPower` or `Opportunistic`
    - [ ] Connection interval appropriate for use case (not always `High` priority)
    
  • SKILL.md 19.4 KB
    ---
    name: mobile-hardware-ble-nfc
    description: BLE scanning/connecting/GATT operations with react-native-ble-plx, NFC tag reading/writing with react-native-nfc-manager, permissions, background mode, battery-efficient patterns
    ---
    
    # BLE & NFC Patterns
    
    > **Quick Guide:** Use `react-native-ble-plx` for BLE (scanning, connecting, GATT read/write/monitor). Use `react-native-nfc-manager` for NFC (NDEF read/write, tag technology access). BLE values are Base64-encoded -- decode before use. NFC operations follow request-technology/operate/cancel-technology lifecycle. Always clean up: remove BLE subscriptions, call `cancelTechnologyRequest()` for NFC, and `destroy()` the BleManager. MTU defaults to 23 bytes (20 usable) -- negotiate higher on Android. iOS auto-negotiates up to 187 bytes.
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST call `destroy()` on BleManager when deallocating resources -- leaking the manager causes native memory leaks and zombie listeners)**
    
    **(You MUST call `discoverAllServicesAndCharacteristics()` after connecting before any read/write/monitor operations -- GATT structure is not available until discovered)**
    
    **(You MUST call `cancelTechnologyRequest()` in a finally block after every NFC operation -- failing to release the NFC session blocks subsequent scans)**
    
    **(You MUST check BLE adapter state (`PoweredOn`) before scanning -- scanning while powered off or unauthorized throws errors silently on some devices)**
    
    **(You MUST remove all BLE subscriptions (scan listeners, characteristic monitors, disconnect listeners) on cleanup -- leaked subscriptions cause crashes after component unmount)**
    
    </critical_requirements>
    
    ---
    
    **Auto-detection:** react-native-ble-plx, BleManager, startDeviceScan, connectToDevice, monitorCharacteristicForDevice, writeCharacteristicWithResponseForDevice, readCharacteristicForDevice, requestMTUForDevice, react-native-nfc-manager, NfcManager, NfcTech, Ndef, requestTechnology, cancelTechnologyRequest, writeNdefMessage, ndefHandler, useCodeScanner BLE, BLE scanning, NFC tag, NDEF record, characteristic notification, GATT
    
    **When to use:**
    
    - Scanning for and connecting to BLE peripherals (IoT sensors, wearables, medical devices)
    - Reading/writing BLE GATT characteristics and monitoring notifications
    - Reading NDEF tags or writing NDEF records to NFC tags
    - Implementing background BLE reconnection with state restoration
    - MTU negotiation for large data transfers over BLE
    - Accessing low-level NFC technologies (NfcA, IsoDep, MifareUltralight)
    
    **When NOT to use:**
    
    - Classic Bluetooth audio/file transfer (different protocol, different libraries)
    - BLE peripheral/server mode (react-native-ble-plx is central-only)
    - Web-based Bluetooth (use Web Bluetooth API)
    - Wi-Fi Direct or peer-to-peer networking
    
    **Key patterns covered:**
    
    - BLE lifecycle: scan, connect, discover services, read/write/monitor, disconnect
    - Battery-efficient scanning with UUID filters and scan modes
    - MTU negotiation (Android explicit, iOS automatic)
    - Characteristic subscriptions (notifications/indications) with cleanup
    - BLE reconnection and disconnect monitoring
    - NFC NDEF read/write lifecycle with technology request/cancel
    - NFC technology types and platform availability (iOS vs Android)
    - Permission handling for both BLE and NFC
    
    **Detailed Resources:**
    
    - [examples/core.md](examples/core.md) - BLE scanning, connecting, GATT operations, disconnect handling
    - [examples/nfc.md](examples/nfc.md) - NFC NDEF reading/writing, technology types, platform differences
    - [reference.md](reference.md) - API quick reference, permission matrix, decision frameworks
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    BLE and NFC are hardware communication protocols with fundamentally different interaction models:
    
    **BLE** is connection-oriented and long-lived. You scan for devices, establish a persistent connection, discover the GATT service/characteristic tree, then read/write/subscribe to characteristics over time. Connections can last minutes to hours. The main challenges are connection lifecycle management, reconnection, and battery-efficient scanning.
    
    **NFC** is session-oriented and brief. You request a technology, tap a tag, perform one operation (read or write), and release the technology. Sessions last seconds. The main challenges are platform differences (iOS vs Android technology support) and ensuring proper cleanup.
    
    **Core principles:**
    
    1. **Lifecycle-driven** -- BLE connections have a strict flow: scan -> connect -> discover -> operate -> disconnect. Skipping steps causes silent failures.
    2. **Subscription-based** -- BLE characteristic monitoring returns Subscription objects that MUST be removed on cleanup. NFC technology requests MUST be canceled in finally blocks.
    3. **Base64-encoded** -- All BLE characteristic values are Base64-encoded strings. Decode before use, encode before write.
    4. **Platform-aware** -- BLE permissions differ significantly between Android versions. NFC technology support varies between iOS and Android. Always check platform capabilities.
    
    **When to use BLE:**
    
    - Continuous communication with a peripheral (sensor readings, device control)
    - Background monitoring (health devices, beacons)
    - Large data transfers requiring MTU negotiation
    
    **When to use NFC:**
    
    - One-tap interactions (read a tag, write a tag, verify identity)
    - Quick data exchange without pairing
    - Tag provisioning or configuration
    
    **When NOT to use either:**
    
    - High-bandwidth streaming (use Wi-Fi or classic Bluetooth)
    - Cross-platform web apps (use Web Bluetooth / Web NFC)
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: BLE Manager Initialization and State
    
    Create one BleManager instance for the app lifetime. Check adapter state before operations.
    
    ```typescript
    import { BleManager, State } from "react-native-ble-plx";
    
    const manager = new BleManager();
    
    // Wait for Bluetooth to be ready
    manager.onStateChange((state) => {
      if (state === State.PoweredOn) {
        // Safe to scan
      }
    }, true); // true = emit current state immediately
    
    // Cleanup on app teardown
    manager.destroy();
    ```
    
    **Why good:** single manager instance, state checked before operations, destroy called on cleanup, `true` flag emits current state immediately so you don't miss the initial PoweredOn
    
    See [examples/core.md](examples/core.md) for full initialization with background mode support and state restoration.
    
    ---
    
    ### Pattern 2: BLE Scanning with Filters
    
    Filter scans by service UUIDs for battery efficiency. Stop scanning as soon as you find the target device.
    
    ```typescript
    const HEART_RATE_SERVICE_UUID = "0000180d-0000-1000-8000-00805f9b34fb";
    const SCAN_TIMEOUT_MS = 10000;
    
    manager.startDeviceScan(
      [HEART_RATE_SERVICE_UUID], // Filter by service UUID -- null scans ALL devices
      { allowDuplicates: false },
      (error, device) => {
        if (error) {
          /* handle */ return;
        }
        if (device?.name === "MyDevice") {
          manager.stopDeviceScan();
          // Connect to device
        }
      },
    );
    
    // Always set a timeout to stop scanning
    setTimeout(() => manager.stopDeviceScan(), SCAN_TIMEOUT_MS);
    ```
    
    **Why good:** UUID filter reduces battery drain, scan timeout prevents indefinite scanning, duplicates disabled to reduce callback noise, scan stopped once target found
    
    See [examples/core.md](examples/core.md) for complete scanning with Android scan modes and permission handling.
    
    ---
    
    ### Pattern 3: BLE Connection and GATT Discovery
    
    Connect, discover services/characteristics, then operate. Always monitor for disconnects.
    
    ```typescript
    const device = await manager.connectToDevice(deviceId, {
      requestMTU: 512, // Android only -- request larger MTU during connection
    });
    
    // MUST discover before read/write/monitor
    await device.discoverAllServicesAndCharacteristics();
    
    // Monitor for unexpected disconnects
    const disconnectSubscription = device.onDisconnected(
      (error, disconnectedDevice) => {
        // Handle reconnection logic
      },
    );
    
    // Cleanup
    disconnectSubscription.remove();
    await device.cancelConnection();
    ```
    
    **Why good:** MTU requested during connection (Android), GATT discovered before operations, disconnect monitored for reconnection, subscription removed on cleanup
    
    See [examples/core.md](examples/core.md) for full connection flow with retry logic and error handling.
    
    ---
    
    ### Pattern 4: Reading and Writing BLE Characteristics
    
    All values are Base64-encoded. Decode after read, encode before write.
    
    ```typescript
    import { decode as atob, encode as btoa } from "base-64";
    
    const SERVICE_UUID = "0000180d-0000-1000-8000-00805f9b34fb";
    const CHAR_UUID = "00002a37-0000-1000-8000-00805f9b34fb";
    
    // Read
    const characteristic = await device.readCharacteristicForService(
      SERVICE_UUID,
      CHAR_UUID,
    );
    const decodedValue = atob(characteristic.value ?? "");
    
    // Write with response (acknowledged)
    const base64Value = btoa("command-data");
    await device.writeCharacteristicWithResponseForService(
      SERVICE_UUID,
      CHAR_UUID,
      base64Value,
    );
    
    // Write without response (faster, no acknowledgment)
    await device.writeCharacteristicWithoutResponseForService(
      SERVICE_UUID,
      CHAR_UUID,
      base64Value,
    );
    ```
    
    **Why good:** named constants for UUIDs, Base64 decode/encode explicit, write-with-response used for reliable delivery, write-without-response available for speed
    
    See [examples/core.md](examples/core.md) for complete read/write patterns with error handling.
    
    ---
    
    ### Pattern 5: Characteristic Monitoring (Notifications)
    
    Subscribe to characteristic value changes. Returns a Subscription that MUST be removed.
    
    ```typescript
    const subscription = device.monitorCharacteristicForService(
      SERVICE_UUID,
      CHAR_UUID,
      (error, characteristic) => {
        if (error) {
          /* handle */ return;
        }
        const value = atob(characteristic?.value ?? "");
        // Process incoming notification
      },
    );
    
    // CRITICAL: Remove on cleanup
    subscription.remove();
    ```
    
    **Why good:** subscription stored for cleanup, error handled in callback, value decoded from Base64
    
    **Gotcha:** Check `characteristic.isNotifiable` or `characteristic.isIndicatable` before monitoring -- not all characteristics support notifications.
    
    See [examples/core.md](examples/core.md) for monitoring with React hooks and cleanup patterns.
    
    ---
    
    ### Pattern 6: MTU Negotiation
    
    Larger MTU = fewer packets for big payloads. iOS negotiates automatically (up to 187 bytes). Android requires explicit request.
    
    ```typescript
    import { Platform } from "react-native";
    
    const DESIRED_MTU = 512;
    const BLE_HEADER_BYTES = 3;
    
    // Request MTU after connection (Android only -- iOS auto-negotiates)
    if (Platform.OS === "android") {
      const updatedDevice = await device.requestMTU(DESIRED_MTU);
      const usableBytes = updatedDevice.mtu - BLE_HEADER_BYTES;
      // usableBytes is the max payload per packet
    }
    ```
    
    **Why good:** platform check avoids unnecessary call on iOS, named constants for MTU and header size, usable bytes calculated correctly (MTU minus 3-byte ATT header)
    
    See [reference.md](reference.md) for MTU size recommendations by use case.
    
    ---
    
    ### Pattern 7: NFC NDEF Read
    
    Request NDEF technology, read the tag, clean up in finally block.
    
    ```typescript
    import NfcManager, { NfcTech } from "react-native-nfc-manager";
    
    // Initialize once at app start
    await NfcManager.start();
    
    async function readNdefTag(): Promise<string | null> {
      try {
        await NfcManager.requestTechnology(NfcTech.Ndef);
        const tag = await NfcManager.getTag();
    
        if (tag?.ndefMessage && tag.ndefMessage.length > 0) {
          // tag.ndefMessage is an array of NDEF records
          // Each record has: tnf, type, id, payload (all number arrays)
          return String.fromCharCode(...tag.ndefMessage[0].payload);
        }
        return null;
      } catch (error) {
        // User cancelled or tag not found
        return null;
      } finally {
        // CRITICAL: Always release the NFC session
        await NfcManager.cancelTechnologyRequest();
      }
    }
    ```
    
    **Why good:** technology request and cancel in try/finally, error handled for user cancellation, start() called once at app initialization, tag null-checked before access
    
    See [examples/nfc.md](examples/nfc.md) for complete NDEF reading with record parsing.
    
    ---
    
    ### Pattern 8: NFC NDEF Write
    
    Request technology, encode the message, write, release.
    
    ```typescript
    import NfcManager, { NfcTech, Ndef } from "react-native-nfc-manager";
    
    async function writeNdefTag(url: string): Promise<boolean> {
      try {
        await NfcManager.requestTechnology(NfcTech.Ndef);
        const bytes = Ndef.encodeMessage([Ndef.uriRecord(url)]);
        await NfcManager.ndefHandler.writeNdefMessage(bytes);
        return true;
      } catch (error) {
        return false;
      } finally {
        await NfcManager.cancelTechnologyRequest();
      }
    }
    ```
    
    **Why good:** Ndef utility encodes records properly, try/finally ensures cleanup, boolean return signals success/failure
    
    See [examples/nfc.md](examples/nfc.md) for text records, multi-record messages, and platform-specific handling.
    
    ---
    
    ### Pattern 9: BLE Permissions
    
    BLE permissions differ significantly across Android versions. iOS requires Info.plist entries.
    
    ```typescript
    import { Platform, PermissionsAndroid } from "react-native";
    
    async function requestBlePermissions(): Promise<boolean> {
      if (Platform.OS === "android") {
        const apiLevel = Platform.Version;
        if (apiLevel >= 31) {
          // Android 12+: BLUETOOTH_SCAN + BLUETOOTH_CONNECT
          const results = await PermissionsAndroid.requestMultiple([
            PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
            PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
          ]);
          return Object.values(results).every((r) => r === "granted");
        }
        // Android <12: ACCESS_FINE_LOCATION
        const result = await PermissionsAndroid.request(
          PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
        );
        return result === "granted";
      }
      // iOS: permissions handled via Info.plist (NSBluetoothAlwaysUsageDescription)
      return true;
    }
    ```
    
    **Why good:** Android API level checked for correct permission set, Android 12+ uses new Bluetooth permissions, pre-12 falls back to location permission, iOS handled via plist
    
    See [reference.md](reference.md) for the full permission matrix across platforms and Android versions.
    
    </patterns>
    
    ---
    
    <decision_framework>
    
    ## Decision Framework
    
    ### BLE vs NFC
    
    ```
    What kind of hardware interaction?
    |
    +-> Continuous connection with a peripheral (sensor, wearable)?
    |   +-> BLE -- long-lived connection with GATT operations
    |
    +-> One-tap read/write (tag, card)?
    |   +-> NFC -- session-based, tap and go
    |
    +-> Background monitoring of nearby devices?
    |   +-> BLE -- background scanning with state restoration
    |
    +-> Quick device provisioning (write config to tag)?
        +-> NFC -- write NDEF record, tap target device
    ```
    
    ### BLE Write Method
    
    ```
    Which write method?
    |
    +-> Data MUST arrive reliably?
    |   +-> writeCharacteristicWithResponseForService (acknowledged, slower)
    |
    +-> Speed matters more than reliability?
    |   +-> writeCharacteristicWithoutResponseForService (fire-and-forget, faster)
    |
    +-> Large payload (> MTU)?
        +-> Negotiate higher MTU first, then use write-with-response
    ```
    
    ### NFC Technology Selection
    
    ```
    What kind of NFC tag?
    |
    +-> Standard NDEF content (URL, text, MIME)?
    |   +-> NfcTech.Ndef (iOS + Android)
    |
    +-> ISO 14443-3A tag (raw commands)?
    |   +-> NfcTech.NfcA (iOS + Android)
    |
    +-> Smart card / ISO 7816 (APDU commands)?
    |   +-> NfcTech.IsoDep (iOS + Android)
    |
    +-> Mifare Classic (Android only)?
    |   +-> NfcTech.MifareClassic
    |
    +-> Mifare Ultralight (Android only)?
        +-> NfcTech.MifareUltralight
    ```
    
    </decision_framework>
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **High Priority Issues:**
    
    - **Skipping `discoverAllServicesAndCharacteristics()` after connecting** -- read/write/monitor calls fail silently or throw errors because GATT structure is not cached
    - **Not removing BLE subscriptions on cleanup** -- leaked subscriptions fire callbacks after component unmount, causing "setState on unmounted component" crashes
    - **Forgetting `cancelTechnologyRequest()` in NFC finally block** -- NFC session stays locked, all subsequent NFC operations fail until app restart
    - **Creating multiple BleManager instances** -- each instance allocates native resources. Create ONE and share it. Call `destroy()` only once on app teardown.
    - **Scanning without UUID filter and without timeout** -- scans all devices indefinitely, drains battery rapidly
    
    **Medium Priority Issues:**
    
    - Not checking `State.PoweredOn` before scanning -- scanning while Bluetooth is off throws errors on some devices and does nothing on others
    - Not handling Base64 encoding/decoding for BLE characteristic values -- raw Base64 strings are not human-readable and cannot be compared directly
    - Using `writeCharacteristicWithoutResponseForService` for critical data -- fire-and-forget may lose data; use write-with-response for reliability
    - Not requesting MTU on Android -- default 23-byte MTU means 20 usable bytes per packet, making large transfers extremely slow
    - Hardcoding service/characteristic UUIDs inline instead of using named constants
    
    **Gotchas & Edge Cases:**
    
    - **iOS BLE device IDs are random UUIDs**, not MAC addresses -- they can change after Bluetooth is toggled or the device restarts. Do not persist iOS device IDs for reconnection; use service UUID scanning instead.
    - **Android 12+ changed BLE permissions** -- `BLUETOOTH_SCAN` and `BLUETOOTH_CONNECT` replaced `ACCESS_FINE_LOCATION` for BLE scanning (set `neverForLocation: true` in Expo config if scanning does not need location)
    - **Android 14+ defaults MTU to 517 bytes** -- `requestMTU` may be unnecessary on newer Android devices. Check `device.mtu` after connection.
    - **iOS auto-negotiates MTU up to 187 bytes** -- calling `requestMTU` on iOS has no effect
    - **`onDeviceDisconnected` fires once per registration** -- you must re-register the listener after each reconnection if you want continued disconnect monitoring
    - **NfcTech.NfcB, NfcF, NfcV are Android-only** -- iOS has different equivalents (Iso15693IOS, FelicaIOS). Always check platform before requesting a technology.
    - **NFC on iOS shows a system scan dialog** -- you cannot customize it beyond the `alertMessage`. On Android, scanning is silent.
    - **BLE `allowDuplicates` is iOS-only** -- Android always emits duplicates. Deduplicate in your scan callback using a Set of device IDs.
    - **HCE (Host Card Emulation) is NOT supported by react-native-nfc-manager** -- use a dedicated library like `react-native-hce` for card emulation
    - **NFC `getTag()` returns the last discovered tag** -- if no tag was tapped during the session, it returns the previous tag. Always request technology first.
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    > **All code must follow project conventions in CLAUDE.md**
    
    **(You MUST call `destroy()` on BleManager when deallocating resources -- leaking the manager causes native memory leaks and zombie listeners)**
    
    **(You MUST call `discoverAllServicesAndCharacteristics()` after connecting before any read/write/monitor operations -- GATT structure is not available until discovered)**
    
    **(You MUST call `cancelTechnologyRequest()` in a finally block after every NFC operation -- failing to release the NFC session blocks subsequent scans)**
    
    **(You MUST check BLE adapter state (`PoweredOn`) before scanning -- scanning while powered off or unauthorized throws errors silently on some devices)**
    
    **(You MUST remove all BLE subscriptions (scan listeners, characteristic monitors, disconnect listeners) on cleanup -- leaked subscriptions cause crashes after component unmount)**
    
    **Failure to follow these rules will cause native crashes, memory leaks, blocked NFC sessions, and battery drain.**
    
    </critical_reminders>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related