npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xphone/sdk

v0.6.0

Published

Standalone TypeScript SDK for building xphone (FiveM) phone plugins — typed runtime, capabilities (audio, Dynamic Island, notifications, Siri, device), iOS UI components, i18n and validation.

Readme

@xphone/sdk

Standalone TypeScript SDK for building xphone (FiveM) phone plugins.

Build a phone app in your own repository, ship it as a normal FiveM resource, and have it appear and run inside xphone — without a copy of the xphone monorepo. Everything you need is in this package: a typed plugin client, capabilities (audio, Dynamic Island, notifications, Siri, device state), an iOS-style Vue component library, i18n and form validation.

  • 🧩 Drop-in — your app is a standalone resource; the host discovers it at runtime.
  • 🔒 Self-contained — no xphone-core source required to build.
  • 🟦 Typed — written in TypeScript; full .d.ts for every entry point.
  • 🎛️ Full control — close/open, toasts, notifications, Dynamic Island, audio & volume, Siri voice intents, and device/theme state.

Install

npm install @xphone/sdk
# peer deps you use:
npm install vue            # if you build a Vue UI (most plugins do)
npm install zod            # if you use @xphone/sdk/validation
npm install -D vite @vitejs/plugin-vue

The fastest way to start is the scaffolder:

npm create @xphone/plugin@latest my-app

vue, pinia and zod are optional peer dependencies — install only what you import. lucide-vue-next (used by the component library) is a regular dependency and is installed for you.


Quick start

// vite.config.ts
import vue from '@vitejs/plugin-vue'
import { createStandalonePluginConfig } from '@xphone/sdk/vite'

export default createStandalonePluginConfig({ plugins: [vue()] })
<!-- src/App.vue -->
<script setup lang="ts">
import { useXphonePlugin } from '@xphone/sdk/vue'

const phone = useXphonePlugin('my-app', { language: 'en' })

function start() {
  phone.toast('Job started', 'success')
  phone.dynamicIsland.set('job', { layout: 'progress', title: 'Working', progress: 0 })
}
</script>

<template>
  <button @click="start">Start</button>
</template>
// src/main.ts
import { createApp } from 'vue'
import '@xphone/sdk/styles'
import App from './App.vue'

createApp(App).mount('#app')

npm run build produces a self-contained ui/dist/ you point your resource's iframe at (see Hosting in xphone).


The plugin client

useXphonePlugin(appId, options?) (Vue) and createXphoneClient(options) (framework-agnostic) return the same client. Capabilities are namespaced; the most common actions also have top-level shortcuts.

const phone = useXphonePlugin('my-app')

// shortcuts
phone.close()
phone.openApp('settings')
phone.toast('Saved', 'success')
phone.notify({ title: 'New message', body: 'Tap to open' })

// capabilities (see below)
phone.dynamicIsland.set(/* … */)
phone.audio.setVolume(0.5)
phone.siri.register(/* … */)
const state = await phone.device.getState()

// talk to your own resource's NUI callbacks
const data = await phone.fetch('getProfile', { id: 1 })

Capabilities

| Namespace | What it does | | --- | --- | | phone.app | close(), open(target?), lifecycle: onOpened/onClosed/onForeground/onBackground | | phone.feedback | toast(message, type) | | phone.notifications | notify({ title, body, groupKey, openAction }) | | phone.dynamicIsland | set/update/end/endAll live activities, onEvent for taps/actions | | phone.audio | setVolume/getVolume, play/stop, duck/unduck, onVolumeChange | | phone.bluetooth | getState(), scan(), connect(id), disconnect(), routeAudio/stopAudio, onChange — play "to Bluetooth" like a real phone (requires host ≥ 0.5) | | phone.siri | register(intents), unregister, onInvoke (return a string to speak back) | | phone.device | getState(), onThemeChange, onLanguageChange | | phone.identity | get() → current player number, balance, avatar, socials | | phone.contacts | list(), find(query) | | phone.calls | dial(target, { video }) (number or contact name) | | phone.camera / phone.gallery | capture() / pick() → result via phone.onPhoto(...) | | phone.location | getPosition(), setWaypoint(x, y) | | phone.maps | open({ x, y, label }) | | phone.notifications.setBadge(n) | set the home-screen icon badge | | phone.app.open(target, payload) / onOpenPayload(...) | deep-link between apps |

const me = await phone.identity.get()           // { number, balance, avatar, … }
const contacts = await phone.contacts.find('jo')
phone.calls.dial('Joana')

phone.onPhoto(({ url, source }) => setAvatar(url))
phone.camera.capture()                           // opens camera; result via onPhoto

const pos = await phone.location.getPosition()
phone.maps.open({ x: pos.x, y: pos.y, label: 'Me' })
phone.notifications.setBadge(3)

Camera/gallery navigate away and reload your iframe, so the result is delivered to onPhoto after your app reopens (the host buffers it — you won't miss it). The Dynamic Island auto-hides while your app is in the foreground and reappears when you leave it, like real iOS — no action needed.

Dynamic Island

phone.dynamicIsland.set('delivery', {
  layout: 'progress',
  title: 'Delivering',
  subtitle: 'ETA 4 min',
  progress: 0.25,
  actions: [{ id: 'cancel', label: 'Cancel' }]
})
phone.dynamicIsland.onEvent(({ event, actionId }) => {
  if (event === 'action' && actionId === 'cancel') phone.dynamicIsland.end('delivery')
})

Audio & volume

phone.audio.setVolume(0.4)          // 0..1
phone.audio.play({ src: 'nui://my-app/sfx/ping.ogg' })
phone.audio.duck()                  // lower host media while you speak
const vol = await phone.audio.getVolume()

Siri voice intents

phone.siri.register([
  { name: 'play', phrases: ['play my playlist', 'start the music'] }
])
phone.siri.onInvoke(({ intent, args }) => {
  if (intent === 'play') {
    startPlayback()
    return 'Playing your playlist'   // spoken back to the user
  }
})

Device & lifecycle

const { theme, language } = await phone.device.getState()
phone.device.onThemeChange((t) => applyTheme(t))
phone.app.onForeground(() => refresh())

Audio, Siri and device capabilities require an xphone host ≥ 0.4. On older hosts these calls are safely ignored.


Receiving data from your resource

Your server/client Lua can push data into the running iframe:

exports['xphone-core']:PushNuiMessage({ action = 'pluginPush', appId = 'my-app', payload = { online = 12 } })
phone.onPush((payload) => {
  console.log('players online:', payload.online)
})

Server side (@xphone/sdk/server)

A CommonJS layer for your plugin's server script — require() it from your server_script (FiveM server scripts run as CJS). It absorbs the manifest lifecycle, schema apply, and a typed UI→server callback bus, plus typed access to identity and banking.

// server/main.js  —  server_script 'server/main.js'
const { definePlugin, identity, banking } = require('@xphone/sdk/server')

definePlugin({
  manifest: {
    id: 'pizza',
    label: 'Pizza LS',
    uiType: 'iframe',
    iframeUrl: `nui://${GetCurrentResourceName()}/ui/dist/index.html`
  },
  schema: true, // applies sql/schema.sql on start (no-op without oxmysql)
  callbacks: {
    // Reached from the UI with phone.server.call('order', payload).
    // ctx already has { src, license, licenseKey } for the caller.
    async order(ctx, payload) {
      const ok = await banking.removeMoney(ctx.license, payload.price)
      return { ok, number: await identity.getNumber(ctx.license) }
    }
  }
})
// In the app (iframe):
const res = await phone.server.call('order', { price: 25 })
if (res.ok) phone.toast('Ordered!')   // res.data holds your handler's return

| API | What it does | | --- | --- | | definePlugin({ manifest, schema?, callbacks? }) | register the app (race-safe on both starts), apply schema, wire callbacks | | identity | licenseFromSource, licenseKey (hex, path-safe), getNumber, resolveLicense, findSourceBy* | | banking | getBalance, addMoney, removeMoney, transfer (atomic), createInvoice | | db | query/insert/update (auto ${DBPrefix}), table(name), applySchema(file?) |

Callbacks may validate input — pass { input: z.object({…}), handler } instead of a bare function (any .parse()-able schema works). Add @xphone/sdk to your plugin's dependencies so the FiveM resource builder installs it server-side.


Entry points

| Import | Contents | | --- | --- | | @xphone/sdk | client, protocol, validation, i18n, design tokens (no Vue) | | @xphone/sdk/server | server tier (CJS): definePlugin, identity, banking, db | | @xphone/sdk/runtime | createXphoneClient, transport, protocol | | @xphone/sdk/vue | useXphonePlugin composable | | @xphone/sdk/components | iOS-style Vue component library | | @xphone/sdk/validation | Zod-powered, i18n-aware forms (usePluginForm, v, z) | | @xphone/sdk/i18n | locale loading & translation | | @xphone/sdk/ui | design tokens (colors, typography, shapes, motion) | | @xphone/sdk/styles | base stylesheet (CSS) | | @xphone/sdk/vite | createStandalonePluginConfig | | @xphone/sdk/dev | bootstrapPluginDev for local dev | | @xphone/sdk/nui | low-level NUI helpers | | @xphone/sdk/protocol | raw wire contract (constants + types) | | @xphone/sdk/plugin | defineXphonePlugin, PluginDefinition (integrated-tier contract) |

Integrated vs standalone tiers

There are two ways to build an app, and they use different parts of this package:

  • Standalone (third-party) — your app is its own resource, installed from npm. It registers on the server with RegisterPhoneAppManifest and talks to the host through createXphoneClient() (audio, maps, dynamic island, notifications, etc. via client.*). Everything documented above is available. Start with npm create @xphone/plugin@latest.
  • Integrated (first-party) — the app lives in the xphone monorepo and is compiled by the host's NUI build. It exports a nui/plugin.js typed with defineXphonePlugin from @xphone/sdk/plugin, and may reach host internals directly through the integrated-only subpaths @xphone/sdk/phone, /audio, /maps, /social, /dynamic-island.

⚠️ The integrated-only subpaths (/phone, /audio, /maps, /social, /dynamic-island) only resolve inside the monorepo (the host wires them via Vite aliases). A standalone plugin that imports one gets a clear error at import time — use createXphoneClient() instead. Likewise, the capability namespaces in the table above (identity, contacts, calls, …) are served by the host: their availability depends on the host version, so feature-detect rather than assume.

Components

import '@xphone/sdk/styles'
import { ValidatedField, IosToggle, IosButton, IosOverlayProvider } from '@xphone/sdk/components'

Validation

import { usePluginForm, v, z } from '@xphone/sdk/validation'

const form = usePluginForm(
  () => z.object({ name: v.requiredString(), age: v.age(18) }),
  { name: '', age: '' },
  { t: phone.t } // validation messages are i18n keys
)

Hosting in xphone

A plugin is a normal FiveM resource. Register it with the host on startup — no edits to xphone-core required:

-- server.lua
AddEventHandler('onResourceStart', function(res)
  if res ~= GetCurrentResourceName() then return end
  exports['xphone-core']:RegisterPhoneAppManifest({
    id        = 'my-app',
    label     = 'My App',
    icon      = ('nui://%s/ui/dist/icon.png'):format(GetCurrentResourceName()),
    uiType    = 'iframe',
    iframeUrl = ('nui://%s/ui/dist/index.html'):format(GetCurrentResourceName()),
    resource  = GetCurrentResourceName()
  })
end)

The icon then appears on the home screen and your index.html is shown in an iframe when the app is opened.


Home-screen widgets

Apps can own home-screen widgets — just like the built-in ones. A widget belongs to your app: it only appears in the widget gallery while your app is installed, and it vanishes if the app is removed. You can declare more than one per app.

Two tiers, matching the two ways to build a plugin:

Standalone (iframe widget)

Declare a widgets array in your server manifest. Each widget points at an HTML page your resource serves; it renders inside a sized card and is display-only (the phone owns taps and long-press — tapping opens your app, long-press enters home customization). Sizes: 2x2 (square) or 4x2 (wide).

exports['xphone-core']:RegisterPhoneAppManifest({
  id        = 'my-app',
  label     = 'My App',
  uiType    = 'iframe',
  iframeUrl = ('nui://%s/ui/dist/index.html'):format(GetCurrentResourceName()),
  resource  = GetCurrentResourceName(),
  widgets   = {
    {
      id       = 'my-app:summary',
      labelKey = 'widget_my_app_summary', -- i18n key (your plugin locales)
      size     = '2x2',
      iframeUrl = ('nui://%s/ui/widget.html'):format(GetCurrentResourceName())
    },
    {
      id       = 'my-app:wide',
      labelKey = 'widget_my_app_wide',
      size     = '4x2',
      iframeUrl = ('nui://%s/ui/widget-wide.html'):format(GetCurrentResourceName())
    }
  }
})

Expose the widget pages in your fxmanifest.lua (files { 'ui/*.html' }). The typed manifest shape is exported as PhoneAppManifest / WidgetManifest from @xphone/sdk/plugin. A complete example lives in phone-apps/xphone-example-sdk.

Integrated (native Vue widget)

A native plugin renders its widget with a Vue component. List widgets on the plugin definition; the host registers each (the widget receives a def prop and emits open).

import { defineXphonePlugin } from '@xphone/sdk/plugin'
import MyApp from './apps/MyApp.vue'
import SummaryWidget from './widgets/SummaryWidget.vue'

export default defineXphonePlugin({
  id: 'my-app',
  component: MyApp,
  widgets: [
    { id: 'my-app:summary', labelKey: 'widget_my_app_summary', size: '2x2', component: SummaryWidget }
  ]
})

opens (optional) sets which app a tap opens — it defaults to your app.


TypeScript

Everything is typed. The wire contract is exported from @xphone/sdk/protocol if you need to type a server push or build a custom transport:

import type { DynamicIslandPayload, SiriIntent, DeviceState } from '@xphone/sdk/protocol'

Building this package (contributors)

npm install
npm run build       # tsup → dist/ (ESM + .d.ts)
npm run typecheck   # vue-tsc, includes the .vue components
npm run verify      # ensures nothing escapes the published package

The TypeScript "logic" layer is compiled to dist/; the Vue components and stylesheet ship as source and are compiled by the consumer's bundler.

License

MIT © Stoshe Labs