@muten/mobile
v0.0.2
Published
Native mobile abilities for muten apps — notifications, haptics, share, background, AdMob banner — used via `use { … } from "@muten/mobile"`. Wraps Capacitor (wired by cap sync) with graceful web fallbacks. Adds abilities to muten from the OUTSIDE; never
Maintainers
Readme
@muten/mobile
Native mobile abilities for muten apps — notifications, haptics,
share, and a background watcher — without touching the muten core. It's a plugin: it adds abilities from the
outside through the two seams muten already exposes — a use import for callable JS (oracle-validated, so
lint == runtime) and Capacitor for the native side (wired by cap sync). On the web every call falls back to the
platform API (Web Notifications / navigator.vibrate / Web Share) or is a harmless no-op, so the same code runs
everywhere.
Install
npm i @muten/mobile
# for the native side, add the Capacitor targets you use:
npm i -D @capacitor/haptics @capacitor/share # local-notifications is used by notify()
npm i @capacitor-community/admob # only if you use showBanner() (AdMob)cap sync discovers @muten/mobile (it declares capacitor.android.src) and wires its native module — the
MobileWatch plugin + a generic foreground WatchService. Nothing else to register.
Use it from muten
# a page or store — `use` the abilities you need:
use notify, vibrate, share, startWatch from "@muten/mobile"
# fire a native notification (tap opens the app at `route`):
effect { notify("Claude has a question", "acme-api — wire up Stripe", "/ask/{s.id}") }
# haptic + native share:
Button "Share" -> shareIt
Button "Buzz" -> buzzAPI
| Function | What it does |
|---|---|
| notify(title, body, route?) | native notification (status bar / lock screen); web → Web Notification. route opens the app there on tap. |
| vibrate(ms?) | haptic feedback; web → navigator.vibrate. |
| share(text, url?) | native share sheet; web → Web Share API. |
| ensureNotifications() | request notification permission once (call on mount before notifying). |
| onNotificationTap(cb) | run cb(route) when a notification is tapped. |
| startWatch(config) / stopWatch() | native background watcher: a foreground service keeps a WebSocket to config.url alive independently of the WebView and fires notifications per config.rules even when the app is backgrounded or closed. Native only; no-op on web. |
| showBanner(adId, opts?) | persistent AdMob banner. opts.test (default true) = Google TEST ads; opts.top puts it at the top. Native only; no-op on web. |
| hideBanner() | remove the banner. |
| isNative() | true inside a real app shell, false on the web. |
startWatch config
startWatch({
url: "ws://192.168.1.20:4599/ws",
arrayKey: "sessions", // json[arrayKey] is the list (default "sessions")
idKey: "id", stateKey: "state", agentKey: "agent", taskKey: "task",
rules: { // fire a notification when a row enters one of these states
awaiting: { title: "Needs your OK" },
asking: { title: "Has a question" },
settled: { title: "Finished" }
}
})AdMob banner
showBanner("<banner-ad-unit-id>") shows a Google AdMob banner (native, pinned to the bottom). Reserve a strip of
the same height in your layout so no game content sits under it.
use showBanner from "@muten/mobile"
effect { showBanner("ca-app-pub-XXXX/YYYY") } # your BANNER ad-unit idTwo ids, two places (from the AdMob console — don't mix them up):
- the ad-unit id (
…/YYYY) → passed toshowBanner()above. - the app id (
…~ZZZZ) → your app'sandroid/app/src/main/AndroidManifest.xml, inside<application>:
The AdMob SDK reads it at startup — without it the app crashes on init.<meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="ca-app-pub-XXXX~ZZZZ"/>
Test ads: showBanner defaults to isTesting: true, so Google serves TEST ads. Keep it during development —
never tap your own LIVE ads (AdMob bans the account for it). Pass { test: false } only in the store build.
Why a plugin, not a core feature
muten is a web-first, platform-agnostic UI DSL — its core never mentions Android. Everything mobile lives here, in the plugin, so the language stays what it is. If you build a web app, you never see a word of this.
