Custom App
Build your own app inside GKSPHONE — registration, lifecycle, and the window.gksphone bridge.
A custom app is your own FiveM resource with a NUI page, registered with the phone and rendered inside it. Start from the template on GitHub.
Register the app
Call AddCustomApp client-side, once, after the phone has started:
exports["gksphone"]:AddCustomApp({
name = "mdt", -- required, unique
appurl = "https://cfx-nui-gksphone-app/ui/index.html", -- required
icons = "/html/img/icons/mdt.png",
description = "Police records",
show = true,
startapp = false,
blockedjobs = {},
allowjob = {},
labelLangs = { en = "MDT", tr = "MDT", de = "MDT" },
})| Field | |
|---|---|
name | Required. Unique. Registering the same name again replaces the previous entry |
appurl | Required. https://cfx-nui-<your-resource>/<path> |
icons | Icon path |
description | Line shown in the App Gallery |
show | Listed in the App Gallery. Defaults to true |
startapp | On the home screen at setup. Defaults to false |
blockedjobs / allowjob | Job restrictions — see Apps |
labelLangs | Display name per language code |
onOpen / onClose | Callbacks fired when the player opens or closes the app — see below |
url is set to /customapp for you — anything you pass is overwritten.
The app closes automatically if your resource stops.
Lifecycle
Run something when the player opens or closes your app. Declare the callbacks inline:
exports["gksphone"]:AddCustomApp({
name = "mdt",
appurl = "https://cfx-nui-gksphone-app/ui/index.html",
onOpen = function(phoneId, phoneNumber) print("opened by", phoneNumber) end,
onClose = function(phoneId, phoneNumber) end,
})Or register them separately, which also lets you replace them later:
exports["gksphone"]:onAppOpen("gksphone-app", function(phoneId, phoneNumber) end)
exports["gksphone"]:onAppClose("gksphone-app", function(phoneId, phoneNumber) end)
exports["gksphone"]:removeAppCallbacks("gksphone-app")These three take your resource name, not the app's name. The phone derives it from appurl — https://cfx-nui-gksphone-app/… gives gksphone-app. Pass the display name and the callback never fires.
Both callbacks receive the phone's unique id and its number.
The window.gksphone bridge
The phone injects window.gksphone into your iframe. It's ready once the page has loaded:
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
if (window.gksphone) console.log('bridge ready')
}, 100)
})| Function | Returns |
|---|---|
isDarkMode() | true when the phone is in dark mode |
onChangeDarkMode(cb) | Registers a callback fired when the player switches theme |
notify(text, timeout) | — Toast. timeout defaults to 2000 ms |
loadingPopup(text) / closeLoadingPopup() | — Blocking spinner |
calling(number, anon) | — Starts a voice call |
videoCall(number) | — Starts a VidMeet call |
CameraOpen(photo, video) | async — media URL, or null if cancelled |
GetGallery(onlyVideo, onlyPicture, multiple, camera) | async — { data, opencamera }, or null |
FullScreenImage(url) | — Opens the image viewer |
SelectEmoji(true) | — Opens the emoji picker |
setStatusBarColor(color) | — Sets the app's background behind the status bar |
fetchNui(event, data) | — Sends an NUI message to the phone |
createMap(container, options) | Map controller — see below |
url | Your resource name, taken from appurl |
Camera and gallery
Both are async — await them:
const photo = await window.gksphone.CameraOpen(true, false) // photo on, video off
if (photo) console.log(photo)
const picked = await window.gksphone.GetGallery(false, true, false, true)
// { data: "<url>", opencamera: false } — a file was picked
// { data: false, opencamera: true } — the player tapped the camera icon insteadEmoji picker
SelectEmoji takes a boolean, not a URL. The selection arrives as a message:
window.gksphone.SelectEmoji(true)
window.addEventListener('message', (event) => {
if (event.data?.type === 'emojiSelected') {
console.log(event.data.eventData)
}
})Game map
const map = gksphone.createMap(document.getElementById('map'), {
zoom: 3,
center: { x: 428.9, y: -984.5 },
map: 'losSantos', // | 'cayoPerico'
style: 'atlas', // | 'satellite' | 'grid'
allowMoving: false
})
await map.ready
map.setZoom(4)
map.setPosition({ x: 100, y: 200 }, 5)
map.setMap('cayoPerico')
map.setStyle('satellite')
await map.setShowSelf(true) // live player pin, polled ~3s
const pin = map.addLocation({ title: 'LSPD', coords: { x: 428.9, y: -984.5 } })
map.removeLocation(pin)
map.destroy() // call this when leaving the viewPlayer position and waypoints go through fetchNui:
gksphone.fetchNui('gksphone:get:location')
gksphone.fetchNui('set:gps', { x, y })Lua-side helpers
NuiSendMessage
Use this instead of SendNUIMessage — it routes the message into the phone's iframe:
exports["gksphone"]:NuiSendMessage({ event = 'showMessage', hello = "world" })window.addEventListener('message', (event) => {
if (event.data?.event === 'showMessage') {
console.log(`Hello ${event.data.hello}!`)
}
})InputChange
Stops the player walking while they type in a field:
exports["gksphone"]:InputChange(true) -- true while focused, false on blur<input type="text" onfocus="inputFocused(true)" onblur="inputFocused(false)">
