Device
@symbiote-native/device wraps
expo-device — physical device
information: brand/model/OS constants, uptime, max-memory, root/jailbreak detection,
side-loading detection, and platform-feature queries — so every SymbioteNative adapter can read
it. Like local auth, every export here is either an
eagerly-resolved constant or a one-shot async call with no per-instance state — no hook/
composable/service to reach for, unlike sensors’ EventEmitter +
live-subscription surface.
| OS platform | Support |
|---|---|
| iOS | ✅ live |
| Android | ✅ live |
| Framework adapter | Support |
|---|---|
| React | ✅ live |
| Vue | ✅ live |
| Angular | ✅ live |
| Svelte | ✅ live |
Installation
Section titled “Installation”pnpm add @symbiote-native/deviceexpo-device 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).
expo-device needs no runtime permission on either platform — every constant and function here
reads plain system/build information, nothing gated by a permission prompt.
All four adapters — React, Vue, Angular, Svelte — re-export the exact same constants and free functions; there is no per-adapter hook/composable/service to reach for, since nothing here holds live state or a subscription.
import { useEffect, useState } from 'react';import { Text, View } from '@symbiote-native/react';import { brand, deviceName, getMaxMemoryAsync, getUptimeAsync, isDevice, modelName, osName, osVersion,} from '@symbiote-native/device/react';
export default function DeviceInfo() { const [uptime, setUptime] = useState<number | null>(null); const [maxMemory, setMaxMemory] = useState<number | null>(null);
useEffect(() => { getUptimeAsync().then(setUptime); getMaxMemoryAsync().then(setMaxMemory); }, []);
return ( <View> <Text>{isDevice ? 'Real device' : 'Simulator/emulator'}</Text> <Text>{`${brand ?? 'unknown'} ${modelName ?? ''}`}</Text> <Text>{`${osName ?? 'unknown OS'} ${osVersion ?? ''}`}</Text> <Text>{deviceName ?? 'unnamed device'}</Text> <Text>{uptime === null ? 'checking uptime…' : `Uptime: ${uptime}ms`}</Text> <Text>{maxMemory === null ? 'checking memory…' : `Max memory: ${maxMemory} bytes`}</Text> </View> );}<script setup lang="ts">import { onMounted, ref } from 'vue';import { Text, View } from '@symbiote-native/vue';import { brand, deviceName, getMaxMemoryAsync, getUptimeAsync, isDevice, modelName, osName, osVersion,} from '@symbiote-native/device/vue';
const uptime = ref<number | null>(null);const maxMemory = ref<number | null>(null);
onMounted(() => { void getUptimeAsync().then(value => (uptime.value = value)); void getMaxMemoryAsync().then(value => (maxMemory.value = value));});</script>
<template> <View> <Text>{{ isDevice ? 'Real device' : 'Simulator/emulator' }}</Text> <Text>{{ `${brand ?? 'unknown'} ${modelName ?? ''}` }}</Text> <Text>{{ `${osName ?? 'unknown OS'} ${osVersion ?? ''}` }}</Text> <Text>{{ deviceName ?? 'unnamed device' }}</Text> <Text>{{ uptime === null ? 'checking uptime…' : `Uptime: ${uptime}ms` }}</Text> <Text>{{ maxMemory === null ? 'checking memory…' : `Max memory: ${maxMemory} bytes` }}</Text> </View></template>import { Component, signal } from '@angular/core';import { Text, View } from '@symbiote-native/angular';import { brand, deviceName, getMaxMemoryAsync, getUptimeAsync, isDevice, modelName, osName, osVersion,} from '@symbiote-native/device/angular';
@Component({ standalone: true, imports: [Text, View], template: ` <View> <Text>{{ isDevice ? 'Real device' : 'Simulator/emulator' }}</Text> <Text>{{ brand ?? 'unknown' }} {{ modelName ?? '' }}</Text> <Text>{{ osName ?? 'unknown OS' }} {{ osVersion ?? '' }}</Text> <Text>{{ deviceName ?? 'unnamed device' }}</Text> <Text>{{ uptime() === null ? 'checking uptime…' : 'Uptime: ' + uptime() + 'ms' }}</Text> <Text>{{ maxMemory() === null ? 'checking memory…' : 'Max memory: ' + maxMemory() + ' bytes' }}</Text> </View> `,})export class DeviceInfo { readonly isDevice = isDevice; readonly brand = brand; readonly modelName = modelName; readonly osName = osName; readonly osVersion = osVersion; readonly deviceName = deviceName;
readonly uptime = signal<number | null>(null); readonly maxMemory = signal<number | null>(null);
constructor() { getUptimeAsync().then(value => this.uptime.set(value)); getMaxMemoryAsync().then(value => this.maxMemory.set(value)); }}There’s no per-instance service to inject() here — every constant/function is a plain
export off the core package, read straight in the constructor or class-field initializer.
<script lang="ts"> import { Text, View } from '@symbiote-native/svelte'; import { brand, deviceName, getMaxMemoryAsync, getUptimeAsync, isDevice, modelName, osName, osVersion, } from '@symbiote-native/device/svelte';
let uptime = $state<number | null>(null); let maxMemory = $state<number | null>(null);
$effect(() => { getUptimeAsync().then(value => (uptime = value)); getMaxMemoryAsync().then(value => (maxMemory = value)); });</script>
<View> <Text>{isDevice ? 'Real device' : 'Simulator/emulator'}</Text> <Text>{`${brand ?? 'unknown'} ${modelName ?? ''}`}</Text> <Text>{`${osName ?? 'unknown OS'} ${osVersion ?? ''}`}</Text> <Text>{deviceName ?? 'unnamed device'}</Text> <Text>{uptime === null ? 'checking uptime…' : `Uptime: ${uptime}ms`}</Text> <Text>{maxMemory === null ? 'checking memory…' : `Max memory: ${maxMemory} bytes`}</Text></View>There’s no per-instance rune to reach for here either — every constant/function is a plain
export off the core package, read straight in the script block or an $effect.
Constants
Section titled “Constants”Resolved once, eagerly, at import time, straight off the native module:
| Field | Type | Description |
|---|---|---|
isDevice |
boolean |
true on a real device, false in a simulator/emulator (always true on web) |
brand |
string | null |
The consumer-visible brand of the hardware, e.g. "google", "Apple" |
manufacturer |
string | null |
The actual device manufacturer, which may differ from brand |
modelId |
string | null |
Internal model identifier, e.g. "iPhone7,2". @platform ios |
modelName |
string | null |
Human-friendly model name, e.g. "Pixel 2", "iPhone XS Max" |
designName |
string | null |
The industrial design name/code name of the device. @platform android |
productName |
string | null |
The device’s overall product name. @platform android |
deviceType |
DeviceType | null |
UNKNOWN/PHONE/TABLET/DESKTOP/TV, determined from screen size on Android |
deviceYearClass |
number | null |
The device year class of the hardware |
totalMemory |
number | null |
Total memory accessible to the kernel, in bytes |
supportedCpuArchitectures |
string[] | null |
Supported processor architecture versions the device expects binaries to target |
osName |
string | null |
The OS name, e.g. "Android", "iOS", "iPadOS" |
osVersion |
string | null |
Human-readable OS version string, e.g. "12.3.1" |
osBuildId |
string | null |
Build ID that more precisely identifies the OS version |
osInternalBuildId |
string | null |
Internal build ID of the OS |
osBuildFingerprint |
string | null |
Full build fingerprint string. @platform android |
platformApiLevel |
number | null |
The Android SDK version currently running. @platform android |
deviceName |
string | null |
Human-readable device name, may be user-set |
Functions
Section titled “Functions”| Signature | Description |
|---|---|
getDeviceTypeAsync(): Promise<DeviceType> |
Same value as the deviceType constant, fetched fresh |
getUptimeAsync(): Promise<number> |
Milliseconds since the device’s last reboot (Android doesn’t count deep-sleep time) |
getMaxMemoryAsync(): Promise<number> |
Maximum memory the Java VM will use, in bytes; the native -1 “no limit” sentinel is normalized to Number.MAX_SAFE_INTEGER. @platform android |
isRootedExperimentalAsync(): Promise<boolean> |
Best-effort root (Android) / jailbreak (iOS) check — bypasses exist on both platforms, so false is not a guarantee |
isSideLoadingEnabledAsync(): Promise<boolean> |
Whether apps can be installed via ACTION_INSTALL_PACKAGE outside the system app store. @platform android |
getPlatformFeaturesAsync(): Promise<string[]> |
Platform-specific feature strings the system reports; resolves [] on iOS/web instead of throwing. @platform android |
hasPlatformFeatureAsync(feature: string): Promise<boolean> |
Whether a specific system feature string is present; resolves false on iOS/web instead of throwing. @platform android |
DeviceType
Section titled “DeviceType”| Field | Value | Description |
|---|---|---|
UNKNOWN |
0 |
An unrecognized device type |
PHONE |
1 |
Mobile phone handsets |
TABLET |
2 |
Tablet computers |
DESKTOP |
3 |
Desktop or laptop computers |
TV |
4 |
TV-based interfaces |
How the wrapper works
Section titled “How the wrapper works”@symbiote-native/device ships zero React/Vue/Angular logic in expo-device itself — its
constants and functions are hand-ported, verbatim, into this package’s own core/, resolving the
native module through expo-modules-core’s requireNativeModule rather than the expo
meta-package this project never installs:
packages/device/src/├── core/ # framework-agnostic: every constant + function above, DeviceType; native-module.ts│ # resolves the native module via expo-modules-core's requireNativeModule├── react/ # @symbiote-native/device/react — export * from '../core'├── vue/ # @symbiote-native/device/vue — export * from '../core'├── svelte/ # @symbiote-native/device/svelte — export * from '../core'└── angular/ # @symbiote-native/device/angular — export * from '../core'Same shape as local auth’s three adapter entries: single-file
re-exports with no lifecycle code at all, since every export here is either an eagerly-resolved
constant or a stateless one-shot call — there is nothing for a hook, composable, or service to
subscribe to or clean up. The native code itself is never vendored or copied —
expo-modules-autolinking resolves it straight out of node_modules (see the native setup
guide).