AbsoluteJS

Device APIs

One import for the camera, location, notifications, files, sharing and the rest of the device. The same call uses the native API in your iOS and Android app and the browser API on the web, and the build installs only what you import.

#Import what you use

Every capability is a named export of @absolutejs/devices. Your code never checks which platform it is on: AbsoluteJS picks the browser, Capacitor or Expo implementation when it builds each target.

TS
import { camera, location, share } from '@absolutejs/devices';
1
You import a capability
Import capabilities by name from @absolutejs/devices in any page or component: .ts, .tsx, .js, .jsx, .svelte or .vue.
2
Sync installs the plugins
The CLI finds those imports and installs the exact native plugin version each one needs. Nothing you don’t import is installed.
bunx absolute mobile sync
3
Permissions are generated
Android permissions, the iOS permission prompts (“Shop uses your camera when you choose to take a photo.”) and the iOS privacy manifest are written for you from your app name.
4
The build checks it
A build stops with a clear message if an imported capability’s plugin is missing, so a store build can never ship without it.
Use named imports
The CLI reads import { camera } and import * as devices with devices.camera. It cannot see a capability chosen at runtime, such as devices[name], so name each one you use.

#Capabilities

Every method returns a promise. Listeners such as onChange resolve to a function that stops listening.

ImportMethodsWhat it does
cameracapability(), permission(), requestPermission(), takePhoto({ direction, transform })Take a photo with the front or rear camera, optionally resized.
photoscapability(), pick({ limit, transform })Let the user choose photos. Only the chosen photos are shared with your app.
documentscapability(op), pick({ accept, limit, maximumBytes }), export({ content, name }), open({ content, name })Pick files, save or share a file you made, or open one in the system viewer. Up to 64 MiB by default.
locationcapability(), permission(), requestPermission({ precision }), current(options), watch(listener, options)Coarse or precise location while the app is open.
localNotificationscapability(), permission(), requestPermission(), schedule(notification), cancel(ids), pending(), onReceived(listener), onAction(listener)Notifications your app schedules on the device, now or later.
pushNotificationscapability(), permission(), requestPermission(), enable(), disable(), onReceived(listener), onAction(listener)Notifications your server sends. Registration is automatic; see Push notifications.
sharecapability(content), share({ title, text, url, dialogTitle })Open the system share sheet.
clipboardcapability(op), readText(), writeText(value)Read and write plain text.
hapticscapability(), impact(style), notification(type), selectionChanged(), vibrate(ms)Taps and buzzes. Where there is no haptic engine, calls do nothing instead of failing.
keyboardcapability(), state(), onChange(listener), dismiss()Whether the on-screen keyboard is open and how tall it is.
systemBarscapability(op), setAppearance(appearance, bar), setVisible(visible, bar)Light or dark status-bar icons, and hiding the status or navigation bar.
platformcapability(), info()OS, phone or tablet, app version and build, locale, reduced motion and safe-area insets.
lifecyclecapability(), state(), onChange(listener), onResume(listener), onRestoredOperation(listener)Active, inactive or in the background, and when the app comes back.
linkscapability(), getLaunchLink(), onOpenLink(listener), openExternal(url)The link that opened the app, links opened while it runs, and opening a page in the browser.
networkcapability(), status(), onChange(listener)Online or offline, and Wi-Fi, cellular or ethernet.
backcapability(), onPress(listener)The Android Back button and gesture.
storagecapability(), get(key), set(key, value), remove(key), keys(), clear()Small preferences as strings, namespaced to your app.
secureStoragecapability(), get(key), set(key, value), remove(key), keys(), clear()Secrets in the iOS Keychain or Android Keystore. Never falls back to plain storage.

#Permissions

Importing a capability or checking it never shows a prompt. permission() reads the current state; requestPermission() asks, so call it from something the user did, like tapping Scan receipt. Taking a photo, reading location and scheduling a notification refuse to run until permission is granted.

promptNot asked yet. requestPermission() shows the system prompt.
grantedThe user allowed it.
limitedAllowed for part of what was asked, such as approximate location.
deniedThe user said no. You can explain why and ask again.
blockedThe system will not ask again. Point the user to the app’s settings.
unavailableThis device or runtime has no such feature.
TS
import { camera, isDeviceError } from '@absolutejs/devices';

// Call from a button press: requesting permission shows the system prompt.
const scanReceipt = async () => {
  const status = await camera.capability();
  if (!status.available) return showUploadFallback(status.reason);

  const permission = await camera.requestPermission();
  if (permission.state !== 'granted') return showCameraHelp(permission);

  try {
    const photo = await camera.takePhoto({
      direction: 'rear',
      transform: { width: 1600, height: 1600, quality: 80 }
    });
    preview.src = photo.webPath;
  } catch (error) {
    if (isDeviceError(error) && error.code === 'cancelled') return;
    throw error;
  }
};

#Availability and errors

capability() tells you whether a feature works here before you offer it. When it is available it also says how: native in the app, web in the browser, or emulated in tests. When it is not, it gives a reason you can show. Calls that fail throw a DeviceError with one of these codes; isDeviceError(error) narrows it.

unsupportedThis runtime has no implementation, such as secureStorage in a browser.
unavailableThe feature exists but cannot be used here, such as the camera during server rendering.
permission-requiredPermission has not been granted yet; call requestPermission() first.
permission-deniedThe user refused when asked.
permission-blockedThe system will not show the prompt again.
cancelledThe user closed the camera, picker or share sheet. Usually not an error to show.
temporarily-unavailableTry again shortly, for example when no location fix is available yet.
failedAnything else the platform reported.

#Where each one works

CapabilityBrowseriOS & AndroidExpo
camera, photos, documents
location
While the app is open; there is no background location.
localNotifications
In the browser they fire while the page is open.
pushNotifications
Web Push in the browser and installed web app.
With PWA
share
Uses the Web Share API where the browser has it.
clipboard
haptics
Mapped to vibration patterns in the browser.
keyboard
Estimated from the visual viewport in the browser.
systemBars
The browser can follow the color scheme but not hide bars. The navigation bar exists only on Android.
platform, lifecycle, links, network, storage
backAndroidAndroid
secureStorage
Never emulated with plain browser storage. In Expo apps, sign-in credentials are kept in Expo SecureStore by Auth.

In the app, links.openExternal opens https and http links in the in-app browser; the web version also accepts mailto: and tel:.

#In your components

The capabilities are plain functions, so they work the same in React, Svelte, Vue, Angular, HTML and HTMX pages. Start listeners when a component mounts and call the returned function when it unmounts.

TSX
import { useEffect, useState } from 'react';
import { network } from '@absolutejs/devices';

export const OfflineBanner = () => {
  const [online, setOnline] = useState(true);

  useEffect(() => {
    let stop: (() => void | Promise<void>) | undefined;
    let active = true;

    void network.status().then((status) => setOnline(status.connected));
    void network
      .onChange((status) => setOnline(status.connected))
      .then((unsubscribe) => {
        if (active) stop = unsubscribe;
        else void unsubscribe();
      });

    return () => {
      active = false;
      void stop?.();
    };
  }, []);

  return online ? null : <p role="status">You are offline</p>;
};
SVELTE
<script lang="ts">
  import { onDestroy, onMount } from 'svelte';
  import { lifecycle } from '@absolutejs/devices';

  let stop: (() => void | Promise<void>) | undefined;

  onMount(async () => {
    stop = await lifecycle.onResume(() => refreshInbox());
  });

  onDestroy(() => {
    void stop?.();
  });
</script>

#Location and storage

Ask for coarse location when a neighbourhood is enough; the system prompt is easier to accept. watch sends positions and errors to one listener until you stop it.

TS
import { location } from '@absolutejs/devices';

const permission = await location.requestPermission({ precision: 'coarse' });
if (permission.state === 'granted') {
  const stop = await location.watch(
    (event) => {
      if (event.type === 'position') moveMarker(event.position);
      else showLocationError(event.error);
    },
    { accuracy: 'balanced' }
  );
  // later, when the map closes
  await stop();
}

storage is for preferences. secureStorage is for anything secret and is only available where the platform can protect it. Sign-in tokens are already kept there for you; see Auth, Sync & HTTP.

TS
import { secureStorage, storage } from '@absolutejs/devices';

await storage.set('theme', 'dark');            // preferences, plain text
await secureStorage.set('pin-hint', hint);     // Keychain or Android Keystore

#Testing

@absolutejs/devices/testing gives you an in-memory device. Install it, drive it from your test, and check what your code did. Its secure storage is an in-memory stand-in, not real encryption.

TS
import { afterEach, expect, test } from 'bun:test';
import { installDeviceAdapter, network } from '@absolutejs/devices';
import { createTestDeviceAdapter } from '@absolutejs/devices/testing';

let cleanup: (() => void) | undefined;
afterEach(() => cleanup?.());

test('shows the offline banner when the connection drops', async () => {
  const device = createTestDeviceAdapter({
    platform: { os: 'android', isNative: true }
  });
  cleanup = installDeviceAdapter(device.adapter);

  const seen: boolean[] = [];
  await network.onChange((status) => seen.push(status.connected));
  device.emitNetwork({ connected: false, connectionType: 'none' });

  expect(seen).toEqual([false]);
});
Emit eventsemitNetwork, emitLifecycle, emitLink, emitBack, emitKeyboard, emitLocation, emitLocalNotification and more drive events into your code.
Control permissionscameraPermission, locationPermission and notificationPermission set what requestPermission() returns and count how often it was called.
Inspect resultsclipboardText, sharedContent, hapticEvents, openedExternalUrls, storage, secureStorage, pendingNotifications and the picked and exported files show what your code did.

#Packages