NX CreativeNX CreativeDocs
Scriptsnx_phone

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:

  1. The page, its scripts, styles and icon listed in your fxmanifest files { }. Do not set a ui_page for it; the phone hosts the page.
  2. One RegisterApp call from your server script.
  3. nxp.js loaded in the page, and nxp.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 example x.my_dice.dice. The whole id can be at most 32 characters, so keep the resource name and name short. A longer id is refused with id_too_long.
  • RegisterApp returns the id, or nil and an error: sdk_disabled, invalid_definition, invalid_resource, invalid_name, id_too_long, resource_not_started, invalid_page, page_not_found, invalid_jobs, invalid_gangs or too_many_apps (8 per resource).
  • Calling RegisterApp again with the same name replaces 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 page or icon is missing from your files. 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 in nx_phone_app_overrides, wins over your jobs and gangs (Installation);
  • Config.Apps.Disabled['x.my_dice.dice'] = true turns your app off;
  • Config.SDK.Enabled = false turns 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 safeTop pixels and the home indicator the bottom safeBottom. The status bar's text follows the phone's theme, so draw your top area in theme colours.
  • nxp.js keeps <html data-nxp-theme="dark|light"> and the CSS variables --nxp-accent, --nxp-safe-top and --nxp-safe-bottom up to date, so plain CSS can follow the phone: html[data-nxp-theme="light"] body { background: #f2f2f7; }.
  • nxp.notify shows 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, timeout and 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. The IsPhoneUnlocked, GetSessionContext and IsAppInstalled server 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 or rgb(). Keep backdrop-filter out of your page.
  • Opening the page in a desktop browser works for layout: window.nxp exists, but nxp.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.
  • SendAppMessage returns false and 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:

FieldLimit
titleRequired on start. Up to 40 characters.
subtitleUp to 60 characters.
valueUp to 12 characters, the short text beside the island.
glyphOne 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.
progress0 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.
actionsLua 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'.
deepLinkLua only. { app, route? }: a built-in app id or your own app's id.
privatetrue hides the content from bystanders and in Streamer Mode.
priority'low', 'normal' or 'high'.
ttlSeconds until it ends by itself, 60 to 28800.
staleSecondsSeconds without an update before the card dims.
alertUpdates 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, false clears an optional field.
  • An app page can have one live activity at a time (a second start answers owner_cap). A Lua resource can have Config.LiveActivities.MaxPerOwner per player. Updates are drawn at most 4 times a second. A tap opens your app.
  • A restart of nx_phone drops every activity. Start yours again from nx_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.

NX Docs