Skip to content

Cellular

@symbiote-native/cellular wraps expo-cellular so every SymbioteNative adapter can read the device’s cellular connection generation and carrier/SIM info. Like brightness and battery, it’s built on expo-modules-core — a pure async-function surface, no EventEmitter or Fabric view involved. Its permission surface (getPermissionsAsync/requestPermissionsAsync) shares the exact same usePermissions() hook/composable/service shape as @symbiote-native/brightness; what’s unique to cellular is that almost the entire carrier/SIM surface is Android-only — iOS exposes none of it.

OS platform Support
iOS ✅ live
Android ✅ live
Framework adapter Support
React ✅ live
Vue ✅ live
Angular ✅ live
Svelte ✅ live
Terminal window
npm install @symbiote-native/cellular

expo-cellular and expo-modules-core come along as regular dependencies, pinned to exact versions — never install either yourself, and never add the expo meta-package to your project (it bundles its own Metro/Babel pipeline, which conflicts with this project’s own).

Every function is already framework-agnostic — import them straight from the package root, on any adapter, with no hook/composable/service in the way:

import { useEffect, useState } from 'react';
import { Text } from '@symbiote-native/react';
import { CellularGeneration, getCellularGenerationAsync, getCarrierNameAsync } from '@symbiote-native/cellular';
export default function CellularInfo() {
const [generation, setGeneration] = useState<CellularGeneration | null>(null);
const [carrierName, setCarrierName] = useState<string | null>(null);
useEffect(() => {
getCellularGenerationAsync().then(setGeneration);
getCarrierNameAsync().then(setCarrierName); // Android only — always null on iOS
}, []);
return <Text>{carrierName ?? `generation ${generation}`}</Text>;
}
import { Pressable, Text } from '@symbiote-native/react';
import { usePermissions } from '@symbiote-native/cellular/react';
export default function CellularPermission() {
const [status, request, get, error] = usePermissions();
return (
<>
<Text>{error ? `check failed: ${error.message}` : (status?.status ?? 'checking…')}</Text>
<Pressable onPress={() => (error ? get() : request())}>
<Text>{error ? 'Retry check' : 'Request permission'}</Text>
</Pressable>
</>
);
}

status stays null both while the first fetch is in flight and after it fails, so error is what tells those two apart; get() re-runs the check without prompting the user.

Signature Description
getCellularGenerationAsync(): Promise<CellularGeneration> Gets the generation of the device’s current cellular connection
allowsVoipAsync(): Promise<boolean | null> Deprecated upstream. Whether the SIM’s carrier allows VoIP calls on its network. @platform android — always null on iOS
getIsoCountryCodeAsync(): Promise<string | null> ISO country code of the current registered operator’s MCC. @platform android — always null on iOS
getCarrierNameAsync(): Promise<string | null> Name of the user’s cellular service provider. @platform android — always null on iOS
getMobileCountryCodeAsync(): Promise<string | null> Mobile country code (MCC) of the current registered operator. @platform android — always null on iOS
getMobileNetworkCodeAsync(): Promise<string | null> Mobile network code (MNC) of the current registered operator. @platform android — always null on iOS
getPermissionsAsync(): Promise<PermissionResponse> Checks the user’s permission for accessing cellular info. Delegates to the native module only on Android; always resolves an already-granted response elsewhere
requestPermissionsAsync(): Promise<PermissionResponse> Asks the user for permission to access cellular info. Same Android-only/elsewhere-granted split as getPermissionsAsync
React (/react) Vue (/vue) Angular (/angular) Svelte (/svelte) Signature Returns
usePermissions usePermissions PermissionsService.connect() usePermissions () React: [PermissionResponse | null, request, get, Error | null] tuple. Vue: { status: Ref<PermissionResponse | null>, error: Ref<Error | null>, request, get }. Angular: Signal<PermissionResponse | null> from connect(), plus error: Signal<Error | null> and request()/get() on the service. Svelte: { status, error, request, get }, status and error getters over PermissionResponse | null / Error | null
Field Type Description
status PermissionResponse | null The permission status last read, null until the first fetch resolves
error Error | null Why the automatic fetch left status at null. Cleared by the next successful get()/request()
request () => Promise<PermissionResponse> Asks the user for the permission, then updates status and clears error. Rejects to its caller when the native call fails
get () => Promise<PermissionResponse> Re-reads the current status without prompting. Same update and rejection behavior as request

React hands those back positionally, as [status, request, get, error], so existing two- and three-element destructuring keeps working. Vue wraps status and error in refs; Angular exposes status through connect() and error as a separate readonly signal on the service; Svelte returns both as getters, read as permissions.status / permissions.error.

Read together, the two fields separate the three states:

status error Meaning
null null Not fetched yet
null Error The automatic fetch failed
PermissionResponse null Fetched

Every variant auto-fetches the current permission status once on mount/connect(), and updates again whenever request/get resolves. A failure in that automatic fetch lands in error instead of escaping as an unhandled rejection; get()/request() called by hand still reject to their caller. Angular’s auto-fetch is latched to at most one run per service instance, so a later connect() never re-fetches: call get() to retry. Byte-for-byte the same shape as brightness’s usePermissions.

Member Value Description
UNKNOWN 0 The device’s connection generation could not be determined
CELLULAR_2G 1 2nd generation (GPRS/EDGE-class) connection
CELLULAR_3G 2 3rd generation (UMTS/HSPA-class) connection
CELLULAR_4G 3 4th generation (LTE-class) connection
CELLULAR_5G 4 5th generation (NR-class) connection
  • A missing READ_PHONE_STATE grant is indistinguishable from an unknown network. Android’s getCellularGenerationAsync catches the SecurityException, logs it natively, and resolves CellularGeneration.UNKNOWN — it never rejects. Check getPermissionsAsync() when UNKNOWN comes back instead of reading it as “no cellular data”.
  • The carrier/SIM getters need the SIM ready, not merely present. All five go through a TelephonyManager accessor that yields null unless simState == SIM_STATE_READY, so a PIN-locked or still-initializing SIM produces exactly the null an iPhone produces — and getCellularGenerationAsync falls to UNKNOWN on the same check.
  • On iOS the generation describes the cellular radio, not the path your traffic takes. It reads the telephony service’s current radio access technology and takes the first entry, so on a dual-SIM device the value can belong to whichever service comes first. Pair it with network’s NetworkStateType if what you actually need is the connection currently in use.

@symbiote-native/cellular ships zero React/Vue/Angular logic in expo-cellular itself — that package’s own types file hard-imports PermissionResponse/PermissionStatus from the expo meta-package (which this project never installs), so its functions, enum, and types are hand-ported, verbatim, into this package’s own core/, changing only that one import line to pull from expo-modules-core instead:

packages/cellular/src/
├── core/ getCellularGenerationAsync + the Android-only carrier/SIM surface +
│ getPermissionsAsync/requestPermissionsAsync; native-module.ts
│ resolves the single `ExpoCellular` native module via
│ expo-modules-core's requireNativeModule
├── react/hooks/use-permissions @symbiote-native/cellular/react
├── vue/composables/use-permissions @symbiote-native/cellular/vue
├── svelte/runes/use-permissions @symbiote-native/cellular/svelte
└── angular/services/permissions.service @symbiote-native/cellular/angular

usePermissions is written once as a shared pattern and reapplied identically in brightness; every other export is a stateless free function, re-exported verbatim by all four adapters, same as local-auth. The native code itself is never vendored or copied — expo-modules-autolinking resolves it straight out of node_modules (see the native setup guide).