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 |
Installation
Section titled “Installation”npm install @symbiote-native/cellularexpo-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).
One-shot functions
Section titled “One-shot functions”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>;}<script setup lang="ts">import { onMounted, ref } from 'vue';import { Text } from '@symbiote-native/vue';import { CellularGeneration, getCellularGenerationAsync, getCarrierNameAsync } from '@symbiote-native/cellular';
const generation = ref<CellularGeneration | null>(null);const carrierName = ref<string | null>(null);
onMounted(() => { void getCellularGenerationAsync().then(value => (generation.value = value)); void getCarrierNameAsync().then(value => (carrierName.value = value)); // Android only});</script>
<template> <Text>{{ carrierName ?? `generation ${generation}` }}</Text></template>import { Component, signal } from '@angular/core';import { Text } from '@symbiote-native/angular';import { CellularGeneration, getCellularGenerationAsync, getCarrierNameAsync } from '@symbiote-native/cellular';
@Component({ standalone: true, imports: [Text], template: `<Text>{{ carrierName() ?? 'generation ' + generation() }}</Text>`,})export class CellularInfo { readonly generation = signal<CellularGeneration | null>(null); readonly carrierName = signal<string | null>(null);
constructor() { getCellularGenerationAsync().then(value => this.generation.set(value)); getCarrierNameAsync().then(value => this.carrierName.set(value)); // Android only }}There’s no per-instance service to inject() here — every function is a plain free function
off the core package, same as @symbiote-native/local-auth.
<script lang="ts"> import { Text } from '@symbiote-native/svelte'; import { CellularGeneration, getCellularGenerationAsync, getCarrierNameAsync } from '@symbiote-native/cellular';
let generation = $state<CellularGeneration | null>(null); let carrierName = $state<string | null>(null);
$effect(() => { getCellularGenerationAsync().then(value => (generation = value)); getCarrierNameAsync().then(value => (carrierName = value)); // Android only — always null on iOS });</script>
<Text>{carrierName ?? `generation ${generation}`}</Text>There’s no per-instance rune to reach for here either — every function is a plain free
function off the core package, called straight from $effect.
Permission
Section titled “Permission”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> </> );}<script setup lang="ts">import { usePermissions } from '@symbiote-native/cellular/vue';
const { status, error, request, get } = usePermissions();</script>
<template> <Text>{{ error ? `check failed: ${error.message}` : (status?.status ?? 'checking…') }}</Text> <Pressable @press="error ? get() : request()"> <Text>{{ error ? 'Retry check' : 'Request permission' }}</Text> </Pressable></template>import { Component, inject } from '@angular/core';import { Pressable, Text } from '@symbiote-native/angular';import { PermissionsService } from '@symbiote-native/cellular/angular';
@Component({ standalone: true, imports: [Pressable, Text], template: ` <Text>{{ error() ? 'check failed: ' + error()?.message : (status()?.status ?? 'checking…') }}</Text> <Pressable (press)="error() ? permissions.get() : permissions.request()"> <Text>{{ error() ? 'Retry check' : 'Request permission' }}</Text> </Pressable> `,})export class CellularPermission { readonly permissions = inject(PermissionsService); readonly status = this.permissions.connect(); readonly error = this.permissions.error;}<script lang="ts"> import { Pressable, Text } from '@symbiote-native/svelte'; import { usePermissions } from '@symbiote-native/cellular/svelte';
const permissions = usePermissions();</script>
<Text> {permissions.error ? `check failed: ${permissions.error.message}` : (permissions.status?.status ?? 'checking…')}</Text><Pressable onPress={() => (permissions.error ? permissions.get() : permissions.request())}> <Text>{permissions.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.
Functions
Section titled “Functions”| 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 |
usePermissions()
Section titled “usePermissions()”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 |
usePermissions() return value
Section titled “usePermissions() return value”| 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.
CellularGeneration
Section titled “CellularGeneration”| 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_STATEgrant is indistinguishable from an unknown network. Android’sgetCellularGenerationAsynccatches theSecurityException, logs it natively, and resolvesCellularGeneration.UNKNOWN— it never rejects. CheckgetPermissionsAsync()whenUNKNOWNcomes 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
TelephonyManageraccessor that yieldsnullunlesssimState == SIM_STATE_READY, so a PIN-locked or still-initializing SIM produces exactly thenullan iPhone produces — andgetCellularGenerationAsyncfalls toUNKNOWNon 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
NetworkStateTypeif what you actually need is the connection currently in use.
How the wrapper works
Section titled “How the wrapper works”@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/angularusePermissions 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).