SDK
Add your own app to nx_phone from another resource. Register a web page, talk to it from Lua, send notifications and show Live Activities.
Any resource can add an app to the phone without editing nx_phone. An
app is an ordinary web page that lives in your resource. The phone
shows it full screen in its app frame and gives it a small runtime,
window.nxp, for talking to the phone. Your page talks to your own Lua
the usual way: fetch('https://<your resource>/<callback>') on the page
and RegisterNUICallback in your client script.
You need three things:
- The page, its scripts, styles and icon listed in your fxmanifest
files { }. Do not set aui_pagefor it; the phone hosts the page. - One
RegisterAppcall from your server script. nxp.jsloaded in the page, andnxp.ready()once the page has drawn.
Register the app
-- server.lua in your resource
local function register()
if GetResourceState('nx_phone') ~= 'started' then return end
local id, err = exports.nx_phone:RegisterApp({
name = 'dice', -- letters, digits, _ and -
label = 'Dice', -- the name under the icon, up to 32 characters
description = 'Roll dice with friends', -- one line in Depot, up to 140
page = 'web/index.html', -- a path inside your resource, listed in files
icon = 'web/icon.webp', -- optional, square, 512 x 512 recommended
category = 'utility', -- comms, social, money or utility
jobs = nil, -- optional: { police = 0, ambulance = 2 } (lowest grade)
gangs = nil, -- optional: { ballas = 1 }
defaultInstalled = false, -- true: new phones get it without a trip to Depot
})
if not id then print('nx_phone refused the app: ' .. tostring(err)) end
end
AddEventHandler('nx_phone:sdkReady', register) -- nx_phone (re)started after you
AddEventHandler('onResourceStart', function(res) -- you started after nx_phone
if res == GetCurrentResourceName() then register() end
end)- The app id is
x.<your resource>.<name>, for examplex.my_dice.dice. The whole id can be at most 32 characters, so keep the resource name andnameshort. A longer id is refused withid_too_long. RegisterAppreturns the id, orniland an error:sdk_disabled,invalid_definition,invalid_resource,invalid_name,id_too_long,resource_not_started,invalid_page,page_not_found,invalid_jobs,invalid_gangsortoo_many_apps(8 per resource).- Calling
RegisterAppagain with the samenamereplaces the definition and keeps the app's place on the home screen. exports.nx_phone:UnregisterApp('dice')removes it. Stopping your resource removes its apps automatically. Open phones update either way.- The server console warns when
pageoriconis missing from yourfiles. Players would otherwise get a blank app or a lettered icon. - Players install the app from Depot unless you set
defaultInstalled = true.
Who sees the app
jobs and gangs work like the phone's own app policies: a player with
one of the jobs or gangs, at the lowest grade listed or above, can
install the app. Leave both out and every player can. The server owner
can still override you:
- a row for your app id in
nx_phone_app_policies, or a per-account row innx_phone_app_overrides, wins over yourjobsandgangs(Installation); Config.Apps.Disabled['x.my_dice.dice'] = trueturns your app off;Config.SDK.Enabled = falseturns off every SDK app.
The page
Load the runtime from the phone:
<script src="https://cfx-nui-nx_phone/web/build/sdk/nxp.js"></script>nxp.ready(); // you have drawn: the phone removes its launch splash
nxp.context().then(function (ctx) {
// { theme: 'dark' | 'light', accent: '#0a84ff', locale: 'en',
// streamer: true | false, safeTop: 54, safeBottom: 34 }
});
var off = nxp.onContext(function (ctx) {}); // the player changed theme, language or Streamer Mode
nxp.onMessage(function (data) {}); // from your client Lua (SendAppMessage)
nxp.notify({ title: 'Double six!', body: 'You rolled 12.' })
.then(function (r) { /* { ok: true } or { ok: false, error } */ });
nxp.call('555-0142') // ring a number from this phone
.then(function (r) { /* { ok: true } or { ok: false, error } */ });
nxp.setBadge(3); // the number on your home screen icon; 0 clears it
nxp.close(); // back to the home screen- Call
nxp.ready()as soon as the first screen is drawn. Without it the splash stays up for 6 seconds. - Your page is full screen. The status bar covers the top
safeToppixels and the home indicator the bottomsafeBottom. The status bar's text follows the phone's theme, so draw your top area in theme colours. nxp.jskeeps<html data-nxp-theme="dark|light">and the CSS variables--nxp-accent,--nxp-safe-topand--nxp-safe-bottomup to date, so plain CSS can follow the phone:html[data-nxp-theme="light"] body { background: #f2f2f7; }.nxp.notifyshows a notification from your app while the player is allowed to use it and has it installed. Titles are cut to 60 characters and bodies to 200, with at most 5 per 10 seconds per app. Errors:rate_limited,not_installed,not_allowed,invalid_request,unknown_app,sdk_disabled,timeout.nxp.call(number, { hideCallerId })rings the number from this phone, just as the keypad does: the call screen opens over your app and the phone's usual rules apply (a SIM, airplane mode, blocked numbers, the call rate limit).{ ok: true }means the call was placed, not that anyone answered. Errors:invalid_number,in_call,no_sim,rate_limited,timeoutand the server's other refusals.- The phone never gives your page the player's number, name, identifier or handset id. Ask your own server for anything you need.
- Your page cannot call the phone's own NUI callbacks; they answer
forbidden. Call your own resource's callbacks, and check on your server that the player may do what the page asks. TheIsPhoneUnlocked,GetSessionContextandIsAppInstalledserver exports help with that. - The phone's browser is Chrome 103.
oklch(),color-mix()and the other CSS Color 4 functions do not render there; use hex orrgb(). Keepbackdrop-filterout of your page. - Opening the page in a desktop browser works for layout:
window.nxpexists, butnxp.context()rejects because no phone answers.
Talk to the page from Lua
-- client.lua in your resource
exports.nx_phone:SendAppMessage('dice', { kind = 'nearby', total = 7 }) -- reaches nxp.onMessage
exports.nx_phone:IsAppOpen('dice') -- true while your app is on the phone's screen- Both work only for your own resource's apps.
SendAppMessagereturnsfalseand drops the message when your app is not on screen (nothing is queued), when the data is larger than 32 KB, or past 30 messages a second. The budget is per resource, so other apps' traffic never uses yours.
A "Call customer" button
From the page, nxp.call is enough:
document.getElementById('call').addEventListener('click', function () {
nxp.call(order.phone).then(function (r) {
if (!r.ok) showError(r.error); // e.g. 'no_sim'
});
});From Lua (your own NUI, a laptop app, a target option), use the client
export StartCall. It opens the phone,
waits for the passcode if the phone is locked, and then calls:
exports.nx_phone:StartCall('555-0142')Notifications while the app is closed
Use the server export Notify with your app id, so the player can mute
your app in Settings like any other:
local id, err = exports.nx_phone:Notify({
source = playerId,
appId = 'x.my_dice.dice',
title = 'Your turn',
message = 'Alex rolled 9.',
})It returns the notification id, or false, 'suppressed' when the player
turned notifications off for your app or does not have it installed.
That is not an error: nothing was stored or shown. Notifications that
arrived before the player removed your app come back if they install it
again.
Live Activities
Something ongoing in your app, such as a countdown or an order, can sit in the phone's island and on its lock screen while the player does other things.
From the page:
nxp.activity.start({ title: 'Dice streak', value: 'x3', glyph: 'star', tint: '#ffd60a' })
.then(function (r) { // { ok: true, id } or { ok: false, error }
if (!r.ok) return;
nxp.activity.update(r.id, { value: 'x4', alert: true }); // alert: the island opens for a moment
nxp.activity.end(r.id); // the lock screen keeps it 2 minutes, marked Ended
});From Lua, use the StartLiveActivity, UpdateLiveActivity,
EndLiveActivity and GetLiveActivities exports:
on the server with the player's source first, on the client without
it. Lua activities can also carry up to two actions and a deepLink,
and hear taps through the
nx_phone:liveActivity:action event.
Content fields:
| Field | Limit |
|---|---|
title | Required on start. Up to 40 characters. |
subtitle | Up to 60 characters. |
value | Up to 12 characters, the short text beside the island. |
glyph | One of bell car taxi bike truck package bag food fuel wrench timer clock map-pin navigation shield police medical ticket star music phone cash. |
tint | #rrggbb only. |
progress | 0 to 1. |
timer | { endsAt } or { startedAt }, epoch milliseconds. The phone counts it, so it needs no updates. |
steps | { labels = { ... }, current }: up to 4 labels of 14 characters, current counts from 1. |
actions | Lua only. Up to 2 of { id, label, style }; id is lowercase letters, digits and _ (up to 16), label up to 16 characters, style is 'default' or 'destructive'. |
deepLink | Lua only. { app, route? }: a built-in app id or your own app's id. |
private | true hides the content from bystanders and in Streamer Mode. |
priority | 'low', 'normal' or 'high'. |
ttl | Seconds until it ends by itself, 60 to 28800. |
staleSeconds | Seconds without an update before the card dims. |
alert | Updates only. true opens the island briefly and plays the tone. |
- Text is shown as plain text. A string over its limit is refused, not
cut, and the whole content may be at most 2048 bytes. In an update,
falseclears an optional field. - An app page can have one live activity at a time (a second
startanswersowner_cap). A Lua resource can haveConfig.LiveActivities.MaxPerOwnerper player. Updates are drawn at most 4 times a second. A tap opens your app. - A restart of
nx_phonedrops every activity. Start yours again fromnx_phone:sdkReady, or when your page loads, if it still matters. - Errors:
invalid_content:<field>,too_big,owner_cap,too_many,not_found,not_owner,not_installed,rate_limited,timeout. - Players can turn Live Activities off, keep them off the lock screen, or block one app in Settings, Notifications, Live Activities. Your calls still succeed; nothing is drawn.
Trust
Install an nx_phone app only if you would install the resource it comes from. An app's page runs inside the phone, so a badly written or hostile app can act as the player inside the phone, the same way any client script of a resource you run already can. The phone checks everything that matters on the server (money, ownership, permissions), so an app can do only what the player could do.