Skip to Content
PhoneCustom apps

Custom Apps

LB Phone allows you to add apps that either have a UI or simply trigger functions when opening the app. To add an app that triggers a function upon opening it, go to lb-phone/config/config.lua and add the app to the Config.CustomApps table, like this:

lb-phone/config/config.lua
Config.CustomApps = { ["app_identifier"] = { -- A unique identifier for the app, not shown to the user name = "App Name", -- The name of the app, shown to the user description = "App Description", -- The description of the app, shown to the user developer = "LB Phone", -- OPTIONAL the developer of the app defaultApp = true, -- OPTIONAL if set to true, app should be added without having to download it, game = false, -- OPTIONAL if set to true, app will be added to the game section size = 59812, -- OPTIONAL in kB images = { "https://example.com/photo.jpg" }, -- OPTIONAL array of images for the app on the app store ui = "resource-name/ui/index.html", -- OPTIONAL icon = "https://cfx-nui-" .. GetCurrentResourceName() .. "/ui/icon.png", -- OPTIONAL app icon price = 0, -- OPTIONAL, Make players pay with in-game money to download the app landscape = false, -- OPTIONAL, if set to true, the app will be displayed in landscape mode keepOpen = true, -- OPTIONAL, if set to true, the app will not close when the player opens the app (only works if ui is not defined) onUse = function() -- OPTIONAL function to be called when the app is opened -- do something end, onServerUse = function(source) -- OPTIONAL server side function to be called when the app is opened -- do something end } }

Custom apps using UI

If you want to use a custom UI for your app, you need to create a seperate script and provide the path of the HTML file and send it as ui.

The recommended way to create an app with UI is to create it using exports. We have template apps  that you can use for reference.

If the user has dark mode enabled, data-theme will be set to dark. Otherwise, it will be set to light.

Adding the app

To add the app, use the AddCustomApp export.

Removing the app

To remove the app, use the RemoveCustomApp export.

Sending a message to the UI

To send a message to the UI, you need to use the SendCustomAppMessage export instead of using SendNUIMessage. You would listen for it the same way in the frontend.

Best practices

Load initial data from the UI

Do not send initial data from onOpen. The callback runs when the phone opens the app, while the iframe may still be loading, so a message sent there can arrive before the UI is ready.

Register a NUI callback in your client script:

---@class DashboardData ---@field displayName string ---@field balance number RegisterNUICallback("getDashboardData", function(_, cb) ---@type DashboardData local data = { displayName = "John Doe", balance = 1000 } cb(data) end)

Request the data when the UI mounts:

type DashboardData = { displayName: string balance: number } const data = await fetchNui<DashboardData>('getDashboardData')

fetchNui automatically targets the resource that registered the app. Use SendCustomAppMessage for later push updates after the UI has loaded.

Imported components & functions

When the app gets loaded on the phone, a few functions are imported into the globalThis object.

NameTypeDescription
resourceNamestringThe name of the resource that added the custom app
appNamestringThe app name
settingsobjectThe settings of the phone
componentsobjectUseful components for the app

Components

The following components can be accessed via globalThis.components. You can view a TypeScript declaration file at lb-reactts/ui/src/components.d.ts .

createGameRender

Creates a game render, which renders the game to a canvas. This is used to create a camera in your app, and should be used with the camera exports.

const gameRender = components.createGameRender(canvas) // set the aspect ratio gameRender.resizeByAspect(9 / 16) // pause the rendering gameRender.pause() // unpause the rendering gameRender.resume() // take a photo const blob: Blob = await gameRender.takePhoto() // take a video const recorder = gameRender.startRecording((blob: Blob) => { const video = URL.createObjectURL(blob) }) await new Promise((resolve) => setTimeout(resolve, 5000)) recorder.stop() // destroy the game render gameRender.destroy()

GameMap

Creates an interactive game map inside an HTML element. The container must have a width and height. GameMap loads its dependencies automatically and uses the maps and styles configured in LB Phone.

type GameMapPosition = [y: number, x: number] | { x: number; y: number } type GameMapOptions = { allowMoving?: boolean center?: GameMapPosition minZoom?: number maxZoom?: number defaultZoom?: number } type GameMapLocation = { id: number title?: string image?: string coords: { x: number y: number } } const container = document.querySelector<HTMLElement>('#map') if (!container) { throw new Error('Map container not found') } const gameMap = new components.GameMap(container, { allowMoving: true, center: { x: 428.9, y: -984.5 }, defaultZoom: 3 } satisfies GameMapOptions) await gameMap.ready const location: GameMapLocation | null = gameMap.addLocation({ title: 'LSPD', image: 'https://example.com/lspd.png', coords: { x: 428.9, y: -984.5 } }) await gameMap.setShowSelf(true) gameMap.setPosition({ x: 428.9, y: -984.5 }, 4) // Call this when the app or component is unmounted gameMap.destroy()

allowMoving defaults to true. Positions can be passed as { x, y } game coordinates or as a [y, x] tuple. ready resolves after the map has loaded. The current player position is available through currentCoords as { x, y }, or null before it has been received.

MethodDescription
setZoom(zoomLevel)Sets the zoom level, clamped to the configured minimum and maximum. Returns whether the value was valid.
getZoom()Returns the current zoom level, or null if the map is not ready and no default zoom was provided.
setPosition(position, zoomLevel?)Centers the map on a position and optionally changes the zoom level. Returns whether the position was valid.
getMaps()Returns the available map IDs. The default IDs are losSantos and cayoPerico; custom maps use customMap0, and so on.
setMap(map)Selects a map using its ID or an object containing an id. Returns whether the map exists.
cycleMap()Selects and returns the next map ID, or null if no maps are available.
getStyles()Returns the style names available for the selected map.
setStyle(style)Selects a map style. Returns whether the style exists and could be selected.
cycleStyle()Selects and returns the next style name, or null if no styles are available.
setShowSelf(show)Shows or hides the player’s live position. Returns a promise that resolves after coordinate updates have been toggled.
addLocation({ title?, image?, coords })Adds a marker and returns its location object, including its generated numeric id, or null for invalid coordinates.
removeLocation(location)Removes a marker using its numeric ID or the location object returned by addLocation. Returns whether the ID was valid.
destroy()Removes the map, event listeners, observers, and player coordinate updates. Call it when the map is no longer being used.

uploadMedia

Uploads media and returns a promise with the URL.

// Upload type can be 'Video' | 'Image' | 'Audio' const url = await components.uploadMedia('Video', blob)

saveToGallery

Saves a URL to the gallery and returns a promise with the ID

const id = await components.saveToGallery(url)

setColorPicker

components.setColorPicker({ onSelect(color) {}, onClose(color) {} })

setPopUp

components.setPopUp({ title: 'Popup Menu', description: 'Confirm your choice', buttons: [ { title: 'Cancel', color: 'red', cb: () => { console.log('Cancel') } }, { title: 'Confirm', color: 'blue', cb: () => { console.log('Confirm') } } ] })

setContextMenu

components.setContextMenu({ title: 'Context menu', buttons: [ { title: 'Phone Notification', color: 'blue', cb: () => { sendNotification({ title: notificationText }) } }, { title: 'GTA Notification', color: 'red', cb: () => { fetchNui('drawNotification', { message: notificationText }) } } ] })

setContactSelector

components.setContactSelector({ onSelect(contact) { components.setPopUp({ title: 'Selected contact', description: `${contact.firstname ?? '??'} ${contact.lastname ?? ''} ${contact.number}`, buttons: [ { title: 'OK' } ] }) } })

setShareComponent

See the AirShare export for what data to send.

components.setShareComponent({ type: 'image', data: { isVideo: false, src: 'https://docs.lbscripts.com/images/icons/icon.png' } })

setEmojiPickerVisible

components.setEmojiPickerVisible({ onSelect: (emoji) => { components.setEmojiPickerVisible(false) components.setPopUp({ title: 'Selected emoji', description: emoji.emoji, buttons: [ { title: 'OK' } ] }) } })

setGifPickerVisible

components.setGifPickerVisible({ onSelect(gif) { components.setPopUp({ title: 'Selected GIF', attachment: { src: gif }, buttons: [ { title: 'OK' } ] }) } })

setGallery

components.setGallery({ includeVideos: true, includeImages: true, allowExternal: true, multiSelect: false, onSelect(data) { components.setPopUp({ title: 'Selected media', attachment: { src: Array.isArray(data) ? data[0].src : data.src }, buttons: [ { title: 'OK' } ] }) } })

setFullscreenImage

components.setFullscreenImage('https://docs.lbscripts.com/images/icons/icon.png')

setHomeIndicatorVisible

components.setHomeIndicatorVisible(true)

Functions

fetchNui(event, data, scriptName?)

fetchNui('test', { foo: 'bar' })

onNuiEvent

Listen for NUI messages sent via SendCustomAppMessage

onNuiEvent('test', (data) => { console.log(data) })

onSettingsChange

Listen for settings changes

onSettingsChange((newSettings) => { console.log(newSettings) })

createCall

createCall({ number: '1234567890', // you can send `company` instead of `number` to call a company videoCall: false, hideNumber: false })