Publishing

Native Capabilities

Device APIs — biometrics, push, haptics, camera, and more — as Alpine magics that degrade to the web.


Overview

The native umbrella plugin exposes device capabilities through $-magics that feel like the rest of Manifest. Each is capability-first: it uses Capacitor inside a wrapped app, and falls back to a web equivalent (or a clean no-op) in the browser — so the same markup runs as a website, an installed PWA, and a native app.

Capabilities: $device (state), $share, $secure, $links, $push, $app, $haptics, $biometric, $camera.


Opting in

Add a native block to manifest.json — an empty object is enough:

manifest.json
{ "native": {} }

Inside a Capacitor container the plugin auto-loads even without the block. You can also load it explicitly with data-plugins="native" on the Manifest script tag. $device (below) is part of core and always available.


$device

Reactive device state — available on every build, not just native.

Property Value
$device.os ios · android · macos · windows · linux
$device.platform web · ios · android (the container, distinct from os)
$device.online true / false, reactive to connectivity changes
$device.standalone installed / standalone display mode
$device.native running inside a native (Capacitor) container
$device.touch coarse-pointer / touch device
<div x-show="!$device.online" class="offline-only">You're offline</div>
<template x-if="$device.native"><button @click="$haptics.impact()">Buy</button></template>

The same state is mirrored on <html> as data-os, data-online, data-standalone, data-native, so you also get CSS/Tailwind variants (ios:, offline:, native:, standalone:) and visibility classes (.native-only, .web-only, .offline-only, .online-only, .standalone-only, plus the OS .ios-only / .no-ios family).


$share

Native share sheet → Web Share API → clipboard. Resolves { shared, method, cancelled? }.

<button @click="$share({ title: 'Manifest', text: 'Look at this', url: 'https://manifestx.dev' })">Share</button>

$secure

Key/value storage backed by the Keychain / Keystore in-app, and a namespaced localStorage fallback on the web (not encrypted there — for real secrets, ship native). Good for session tokens.

await $secure.set('token', value)
const token = await $secure.get('token')   // string | null
await $secure.remove('token')
await $secure.keys()                        // string[]
await $secure.clear()

Install a Capacitor secure-storage plugin registered as SecureStorage. If its API differs, adapt it once with $secure.use(adapter).


Deep and universal links. A tapped link (native appUrlOpen, or a cold-start launch URL) hands off to the router automatically.

$links.open('https://app.example.com/order/123')   // navigate programmatically
$links.on(link => { /* take over: link.url, link.path */ })
$links.last                                          // last inbound URL (reactive)

Universal (https://) links map to their path directly. For custom schemes (myapp://order/123) the host+path becomes the route (/order/123); use $links.on() for bespoke mapping.


$push

Push notifications, designed as three author-controlled contracts — not display-only.

// 1. Permission, on your schedule (never auto-prompted)
const state = await $push.request()        // 'granted' | 'denied' | 'prompt' | 'unsupported'

// 2. Device token -> your backend
$push.onToken(token => sendToServer(token))
await $push.register()                       // registers with APNs (native)

// 3. Tap -> route (default routes the payload's url/route; override to customise)
$push.onTap(n => { /* n.data */ })
$push.onReceive(n => { /* foreground */ })

$push.permission   // reactive
$push.token        // reactive

On the web, permission maps to the Notification API; full web push (service worker + VAPID) is out of scope, so register() is a no-op there. The tap → route handoff reuses $links, so it pairs with deep links and $app.


$app

App lifecycle — foreground/background.

<span x-text="$app.active ? 'active' : 'background'"></span>
$app.onChange(active => { if (active) refresh() })

Native uses Capacitor's appStateChange; the web falls back to visibilitychange / pageshow / pagehide.


$haptics

Tactile feedback. Native uses Capacitor Haptics; the web falls back to navigator.vibrate (which desktop and iOS Safari often ignore — it's an enhancement).

$haptics.impact()        // 'LIGHT' | 'MEDIUM' | 'HEAVY'
$haptics.notification()  // 'SUCCESS' | 'WARNING' | 'ERROR'
$haptics.selection()
$haptics.vibrate(200)

$biometric

Face ID / Touch ID — a genuine native capability (and an easy Guideline 4.2 win). The web has no equivalent one-shot verify, so it reports unsupported.

if (await $biometric.available()) {
  const r = await $biometric.verify({ reason: 'Confirm payment' })
  if (r.verified) { /* proceed */ }
}

Install a Capacitor biometric plugin (e.g. capacitor-native-biometric or @aparajita/capacitor-biometric-auth).


$camera

Capture or pick a photo. Native uses Capacitor Camera; the web falls back to a file picker. Resolves { dataUrl?, format?, cancelled? }.

const photo = await $camera.photo()   // capture
const pick  = await $camera.pick()    // from library
if (photo.dataUrl) img.src = photo.dataUrl

Native feel & packaging

Pair these with the native-feel primitives — safe-area utilities (p-safe, pb-safe, --safe-*), the <nav dock> bottom navigation, and viewport-fit=cover (shipped in the starter). Then wrap and submit:

  • Native Apps — packaging with Capacitor.
  • App Store Readiness — passing iOS review, the 4.2 checklist, privacy manifests, and the payments boundary.