Works with Expo ββ’β Read the Documentation ββ’β Report Issues
munim-wifi is a comprehensive React Native Wi-Fi library for nearby-network discovery, current-network information, connection flows, and Wi-Fi fingerprinting. It exposes SSIDs, BSSIDs, signal strength, frequencies, channels, security information, local IP data, and platform-native connect/disconnect behavior where the operating system permits it.
Fully compatible with Expo! It includes an Expo config plugin and a managed Expo example app. Because the package contains native code, Expo projects must use a development build rather than Expo Go.
Built with React Native's Nitro Modules architecture using Swift on iOS, Kotlin on Android, generated native bindings, and callback-based continuous results without a legacy React Native event bridge.
Note: Wi-Fi is heavily platform-gated. Android exposes nearby scans but throttles their frequency. Ordinary iOS apps cannot perform general Wi-Fi scans, so iOS returns the current network when Apple allows access. Unsupported data is returned as null instead of being fabricated.
- π Documentation
- π Features
- Platform Support Matrix
- π¦ Installation
- Permissions and OS Behavior
- β‘ Quick Start
- π§ API Reference
- π Usage Examples
- π Troubleshooting
- π Contributing
- π License
Learn about building Wi-Fi-aware apps in our documentation!
- π‘ Nearby Network Scanning: Retrieve Android scan results without blocking a native thread.
- πΆ Signal Information: Read RSSI, frequency, and calculated 2.4/5/6/60 GHz channel information on Android.
- π Security Details: Read capabilities and an easy-to-use secure/open flag.
- π Continuous Results: Subscribe to result batches or individual networks through Nitro callbacks.
- π§ Wi-Fi Fingerprinting: Capture visible networks with a millisecond timestamp.
- π Native Connection Flows: Android
WifiNetworkSpecifierand iOSNEHotspotConfiguration. - π± Current Network Information: Read SSID, BSSID, IP address, gateway, DNS, and subnet data where available.
- π Local Routing: Android 10+ binds the app process to the approved requested network until
disconnect(). - β Explicit Failures: Invalid options, missing permissions, disabled Wi-Fi, timeouts, and unsupported WEP flows reject clearly.
- π± Cross-platform: One TypeScript API with honest platform-specific results.
- π― TypeScript Support: Full result, option, callback, and HybridObject types.
- β‘ High Performance: Nitro Modules with generated Swift/Kotlin/C++ bindings.
- π Expo Compatible: Managed config plugin and Expo 57 example project.
- π Permission Handling: Android runtime permission requests and real iOS location authorization.
- π§ͺ Release Verification: Package, example, iOS, and Android release-candidate checks.
| Capability | iOS | Android | Notes |
|---|---|---|---|
| Nearby-network scan | β Full | Ordinary iOS apps cannot enumerate nearby Wi-Fi networks. | |
| SSID and BSSID | β | β | iOS requires the Wi-Fi Information entitlement plus an Apple access criterion. |
| RSSI | β | β | Android returns dBm. |
| Frequency and channel | β | β | Android covers 2.4, 5, 6, and 60 GHz channel calculations. |
| Capabilities/security | β | iOS does not expose Android-style capability strings. | |
| Current network | β | β | Values can be hidden by permissions or OS privacy behavior. |
| Local IPv4 address | β | β | Returns null when no Wi-Fi interface is available. |
| Connect | β | β | Both platforms use system-controlled user-consent flows. |
| Disconnect | β | iOS cannot force-disconnect arbitrary saved networks. | |
| Continuous scan | β | Android scan throttling still applies. | |
| Wi-Fi fingerprint | β | No location is inferred by the library. | |
| Security type | β Full | Android classifies WPA2/WPA3/OWE/EAP/Passpoint from scan capabilities. | |
| Local network request | β
joinOnce configuration |
β Android 10+ specifier | Structured ConnectionOutcome on both platforms. |
| Persistent configuration | β
NEHotspotConfiguration |
configureNetwork(). |
|
| Network suggestions | β unsupported outcome |
β Android 10+ | NEHotspotConfiguration is the iOS analog. |
| Wi-Fi settings intent | β | β Android 10+ | requestUserSavedNetwork() opens the system panel. |
| Local-only hotspot | β | β Android 8+ | Returns generated SSID/passphrase/security. |
| Network diagnostics | β Capability + link level | validated/captivePortal are Android-only. |
|
| Network observer | β
NWPathMonitor |
β Default network callback | Continuous NetworkDiagnostics updates. |
| Capability report | β | β | getWifiCapabilityStatus() includes permission states. |
Platform support can vary by OS version, hardware, permission state, foreground/background state, and device-management policy.
npm install munim-wifi react-native-nitro-modules
# or
yarn add munim-wifi react-native-nitro-modulesnpx expo install munim-wifi react-native-nitro-modulesImportant: This package requires a native Expo development build and does not work in Expo Go. After installing, run
npx expo run:ios,npx expo run:android, or create a development build with EAS.
Add the included config plugin to app.json:
{
"expo": {
"plugins": [
[
"munim-wifi",
{
"locationPermission": "Allow this app to find nearby Wi-Fi networks."
}
]
]
}
}The plugin adds the Android Wi-Fi, location, and Nearby Wi-Fi Devices permissions; the iOS location description; and the iOS Access Wi-Fi Information and Hotspot Configuration entitlements.
Generate or rebuild native projects after changing the plugin configuration:
npx expo prebuild
npx expo run:ios
# or
npx expo run:androidBare React Native apps must enable these capabilities in Xcode:
- Access Wi-Fi Information
- Hotspot Configuration
Add a location usage message to Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app uses location permission to access Wi-Fi information.</string>Bare React Native apps should merge these permissions into the application manifest:
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />The Expo config plugin adds them automatically.
Apple's NEHotspotNetwork.fetchCurrent() returns a network only when the app has the Access Wi-Fi Information entitlement and meets at least one qualifying condition, such as precise-location authorization, a network configured by the app, an active VPN configuration, or an active DNS settings configuration. See Apple's fetchCurrent documentation.
disconnect() can remove only a Wi-Fi configuration created by the app. It cannot remove or force-disconnect a network configured by the user or another app.
Wi-Fi scans and scan results require precise-location permission. Android 13+ connection management also uses the Nearby Wi-Fi Devices runtime permission. See Android Wi-Fi permissions.
Android throttles WifiManager.startScan(). Foreground apps can still receive cached results when the OS declines a fresh scan, so the timestamp field records when this package converted the result, not when the radio last observed it.
Android 10+ connections use WifiNetworkSpecifier. The OS presents a system approval flow and may create a local-only connection. The library binds the app process to the approved network until disconnect() so app traffic can reach that network.
import {
getCurrentNetwork,
isWifiEnabled,
requestWifiPermission,
scanNetworks,
} from 'munim-wifi'
const enabled = await isWifiEnabled()
if (!enabled) throw new Error('Wi-Fi is unavailable')
const hasPermission = await requestWifiPermission()
if (!hasPermission) throw new Error('Wi-Fi permission was not granted')
const [current, networks] = await Promise.all([
getCurrentNetwork(),
scanNetworks({ maxResults: 30, timeout: 10_000 }),
])
console.log('Current network:', current)
networks.forEach((network) => {
console.log(network.ssid, network.bssid, network.rssi, network.channel)
})import {
addNetworksFoundListener,
addScanErrorListener,
startScan,
stopScan,
} from 'munim-wifi'
const removeResults = addNetworksFoundListener((networks) => {
console.log('Updated networks:', networks)
})
const removeError = addScanErrorListener(console.warn)
startScan({ interval: 30_000, maxResults: 30 })
// Later:
stopScan()
removeResults()
removeError()import { connectToNetwork, disconnect } from 'munim-wifi'
await connectToNetwork({
ssid: 'Workshop Wi-Fi',
password: 'correct-horse-battery-staple',
timeout: 30_000,
})
// Release/remove the app-managed connection later.
await disconnect()Android 10+ and iOS both show system-controlled approval UI. WEP is unsupported on Android 10+.
Checks whether Wi-Fi appears available to the app.
Returns: Promise<boolean>
On iOS this is inferred from current-network access because Apple does not expose a public Wi-Fi enabled-state API.
Requests precise-location and Nearby Wi-Fi Devices permissions on supported Android versions. On iOS, requests When In Use location authorization when it has not been determined.
Returns: Promise<boolean>
Runs one Android scan or one iOS current-network lookup.
Parameters:
maxResults?(number): Positive integer result limit.timeout?(number): Android timeout from 250 to 30,000 milliseconds.
Returns: Promise<WifiNetwork[]>
Starts repeated Android scans. On iOS, emits one current-network result because general scanning is unavailable.
Parameters:
maxResults?(number): Positive integer result limit.interval?(number): 10,000 to 600,000 milliseconds. Defaults to 30,000.timeout?(number): Validation-compatible one-shot timeout value.
Use addNetworksFoundListener(), addNetworkFoundListener(), or addScanErrorListener() before starting.
Stops continuous Android scanning and releases its broadcast receiver.
Returns visible SSIDs from the current scan information.
Returns: Promise<string[]>
Returns visible/current networks and a millisecond timestamp.
Returns: Promise<WifiFingerprint>
Returns Android signal strength in dBm or null. iOS returns null.
Returns the BSSID matching an SSID or null.
Returns Android { channel, frequency } data or null. iOS returns null.
Returns complete information for the first matching SSID or null.
Returns CurrentNetworkInfo or null. Depending on the platform, it can contain SSID, BSSID, IP address, subnet mask, gateway, and DNS servers.
Returns the current Wi-Fi interface's local IPv4 address or null.
Starts the native connection flow.
Parameters:
ssid(string): Required network name, limited to 32 UTF-8 bytes.password?(string): WPA/WPA2 or WEP password, limited to 64 UTF-8 bytes.isWEP?(boolean): Legacy WEP mode; unsupported on Android 10+.security?(WifiSecurityType): Optional explicit security type. When omitted, security is inferred frompassword/isWEP(open without a password, WPA2 with one). Pass'wpa3'to allow short SAE passphrases and usesetWpa3Passphraseon Android.bssid?(string): Optional Android 10+ BSSID constraint.joinOnce?(boolean): Temporary connection behavior. Defaults totrue; set explicitly tofalseto retain a saved configuration where supported.timeout?(number): Connection timeout from 5,000 to 120,000 milliseconds on iOS and Android.
Returns: Promise<void>
iOS settles each attempt once, verifies the resulting SSID when public APIs expose it, and removes only a newly created persistent configuration after a failed or timed-out attempt. Existing saved configurations are preserved.
Android releases the requested network and clears process binding. iOS removes the app-created configuration for the current SSID.
Returns: Promise<void>
These functions never reject on ordinary platform limitations β they resolve
with a structured outcome whose status explains what happened
('connected' | 'configured' | 'presented' | 'released' | 'unsupported' | 'cancelled' | 'failed').
Requests a temporary, app-scoped connection to a nearby network.
- Android 10+:
WifiNetworkSpecifier+ConnectivityManager.requestNetwork. SetbindProcess: trueto route this process's traffic through the network. The returnedleaseIdreleases the request later. - iOS:
NEHotspotConfigurationwithjoinOnce: true. The returnedleaseIdis the SSID.
Parameters: { ssid, security, bssid?, timeout?, bindProcess? } where security is one of
{ type: 'open' } | { type: 'owe' } | { type: 'wep', passphrase } | { type: 'wpa2', passphrase } | { type: 'wpa3', passphrase }.
Returns: Promise<ConnectionOutcome> β status: 'connected' on success with mode: 'localNetwork'.
Persists a network configuration.
- iOS: persistent
NEHotspotConfiguration(configurationIdis the SSID). - Android 10+: routed through a network suggestion, since Android has no direct managed-configuration equivalent.
Returns: Promise<ConnectionOutcome> β status: 'configured' with mode: 'managedConfiguration'.
- Android 10+: opens the system Wi-Fi panel (or the Android 11+ "add networks" flow when options are given) and resolves
status: 'presented'. - iOS: resolves
status: 'unsupported'.
Releases a requestLocalNetwork lease (Android unregisters the callback and unbinds the process) or removes a configuration (removeConfiguration(forSSID:) on iOS). Resolves status: 'released'.
addNetworkSuggestion(options) / removeNetworkSuggestion(options) / getNetworkSuggestionStatus(options)
Android 10+ WifiManager network suggestions with open, owe, wpa2, and wpa3 support plus hidden and appInteractionRequired flags. Status values include 'added' | 'alreadyExists' | 'removed' | 'notFound' | 'active' | 'inactive'. On iOS these resolve status: 'unsupported' β NEHotspotConfiguration (configureNetwork) is the closest analog. Enterprise and Passpoint credentials resolve 'unsupported' on both platforms.
Returns: Promise<SuggestionOutcome>
Android 8+ local-only hotspot (requires location and, on Android 13+, Nearby Wi-Fi Devices permissions). The outcome carries the generated ssid, passphrase, and securityType. iOS resolves status: 'unsupported'.
Returns: Promise<HotspotOutcome>
Reports per-capability availability (scan, localNetworkRequest, managedConfiguration, networkSuggestions, userSavedNetworkIntent, localOnlyHotspot, wifiDirect, wifiAware, wifiRtt) and permission states (locationPermission, nearbyWifiPermission, wifiInformationPermission) for the current OS version.
Returns: Promise<WifiCapabilityStatus>
One-shot snapshot of the default network.
- Android:
NetworkCapabilities(validated,captivePortal,metered,constrained) plusLinkProperties(interface, addresses, DNS servers, routes, MTU). - iOS:
NWPathMonitor(meteredfromisExpensive,constrainedfromisConstrained);validated/captivePortalare not detectable and stay unset.
Returns: Promise<NetworkDiagnostics>
Continuous NetworkDiagnostics updates as the default network appears, changes capabilities, or is lost (state: 'available' | 'lost' | 'unavailable'). addNetworkObserverListener multiplexes many JS listeners over one native observer and returns a cleanup function.
| API/event | Payload | Notes |
|---|---|---|
addNetworkFoundListener(callback) |
WifiNetwork |
Called once for every network in a result batch. |
addNetworksFoundListener(callback) |
WifiNetwork[] |
Called once per continuous result batch. |
addScanErrorListener(callback) |
string |
Continuous-scan error message. |
addEventListener('networkFound', callback) |
WifiNetwork |
Generic listener alias. |
addEventListener('networksFound', callback) |
WifiNetwork[] |
Generic listener alias. |
addEventListener('scanError', callback) |
string |
Generic listener alias. |
Each listener function returns a cleanup function. addListener() and removeListeners() remain deprecated compatibility shims.
type WifiSecurityType =
| 'open'
| 'owe'
| 'wep'
| 'wpa2'
| 'wpa3'
| 'enterprise'
| 'passpoint'
| 'unknown'
interface WifiNetwork {
ssid: string
bssid: string
rssi?: number
frequency?: number
channel?: number
capabilities?: string
isSecure?: boolean
securityType: WifiSecurityType
timestamp?: number
}
interface CurrentNetworkInfo {
ssid: string
bssid: string
securityType: WifiSecurityType
ipAddress?: string
subnetMask?: string
gateway?: string
dnsServers?: string[]
}
interface WifiFingerprint {
networks: WifiNetwork[]
timestamp: number
location?: { latitude?: number; longitude?: number }
}
interface ConnectionOutcome {
status: ConnectionStatus // 'connected' | 'configured' | 'presented' | 'released' | 'unsupported' | 'cancelled' | 'failed'
mode: ConnectionMode // 'localNetwork' | 'managedConfiguration' | 'userSavedNetwork'
ssid?: string
leaseId?: string
configurationId?: string
boundProcess: boolean
message?: string
}
interface SuggestionOutcome {
status: SuggestionStatus
suggestionId?: string
message?: string
}
interface HotspotOutcome {
status: HotspotStatus // 'started' | 'stopped' | 'unsupported' | 'failed'
reservationId?: string
ssid?: string
passphrase?: string
securityType: WifiSecurityType
message?: string
}
interface NetworkDiagnostics {
timestamp: number
state: NetworkState // 'available' | 'lost' | 'unavailable'
validated?: boolean
captivePortal?: boolean
metered?: boolean
constrained?: boolean
currentNetwork?: CurrentNetworkInfo
linkProperties?: NetworkLinkProperties
}All public result, option, callback, and HybridObject types are exported from the package.
import { getWifiFingerprint, requestWifiPermission } from 'munim-wifi'
if (await requestWifiPermission()) {
const fingerprint = await getWifiFingerprint()
console.log('Captured at', new Date(fingerprint.timestamp))
fingerprint.networks.forEach(({ ssid, bssid, rssi }) => {
console.log(ssid, bssid, rssi)
})
}import { useEffect, useState } from 'react'
import { Button, FlatList, Text, View } from 'react-native'
import {
requestWifiPermission,
scanNetworks,
type WifiNetwork,
} from 'munim-wifi'
export function NetworkScanner() {
const [networks, setNetworks] = useState<WifiNetwork[]>([])
const [message, setMessage] = useState('Ready')
const scan = async () => {
try {
if (!(await requestWifiPermission())) {
setMessage('Permission denied')
return
}
setNetworks(await scanNetworks({ maxResults: 50, timeout: 10_000 }))
setMessage('Scan complete')
} catch (error) {
setMessage(error instanceof Error ? error.message : String(error))
}
}
useEffect(() => () => setNetworks([]), [])
return (
<View>
<Button title="Scan Wi-Fi" onPress={scan} />
<Text>{message}</Text>
<FlatList
data={networks}
keyExtractor={(network) => network.bssid || network.ssid}
renderItem={({ item }) => (
<Text>{item.ssid}: {item.rssi ?? 'β'} dBm</Text>
)}
/>
</View>
)
}- The scan returns no Android networks: Confirm Wi-Fi is enabled, precise-location permission is granted, and device Location Services are enabled. Android can also throttle repeated scans.
- Android 13+ connection throws a permission error: Request Nearby Wi-Fi Devices permission with
requestWifiPermission()before connecting. - iOS returns
nullfor the current network: Verify the Access Wi-Fi Information entitlement, precise-location authorization, and Apple'sfetchCurrent()eligibility conditions. - iOS returns no RSSI/channel/frequency: Those values are not exposed to ordinary iOS apps. This is expected.
- The Android connection cannot reach a local device: Keep the connection active and do not call
disconnect()until local traffic is finished; the package binds the app process to the approved network. - WEP fails on modern Android:
WifiNetworkSpecifierdoes not support WEP. Use WPA2/WPA3 or an open network.
- This package does not work in Expo Go; create a development build.
- Run
npx expo prebuild --cleanafter changing plugin options or upgrading the package. - If iOS capabilities are missing, inspect the generated
.entitlementsfile after prebuild. - If Android permissions are missing, inspect the merged application manifest rather than only the library manifest.
The example app in example/ requests permission, displays current-network information, scans, and renders native result fields. Run it with:
npm install
npm --workspace munim-wifi-example run prebuild
npm --workspace munim-wifi-example run ios
# or
npm --workspace munim-wifi-example run androidSee CONTRIBUTING.md for contribution guidelines. Before opening a pull request, run:
npm install
npm run codegen
npm run build
npm run typecheck:example
npm pack --dry-runDo not edit files in nitrogen/generated directly. Change src/specs/munim-wifi.nitro.ts and rerun npm run codegen.
Releases run locally and do not require GitHub Actions. On the configured maintainer Mac, npm run release:local reads the npm publishing token from macOS Keychain and the GitHub token from the authenticated GitHub CLI session, then runs semantic-release. The credentials are never stored in this repository.
Use npm run release:local -- --dry-run to verify the next release without publishing it.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.