> For the complete documentation index, see [llms.txt](https://grp-development.gitbook.io/titan-scripts/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://grp-development.gitbook.io/titan-scripts/resources/titan-phone/custom-apps.md).

# Custom apps

Add your own apps to TITAN Phone from any resource.

Any resource can add its own app to TITAN Phone. The app appears on the Home Screen, in the App Market, in search, in split view and in the notification settings, just like a built-in app. The phone loads your app's web page inside its screen, and your resource stays in full control of what the page does.

```lua
Config.CustomApps = {
    Enabled = true,
    MaxApps = 24,
    AllowExternalUi = false,
}
```

| Setting           | Description                                                                                   |
| ----------------- | --------------------------------------------------------------------------------------------- |
| `Enabled`         | `false` ignores every app other resources try to add.                                         |
| `MaxApps`         | Most custom apps the phone lists at the same time.                                            |
| `AllowExternalUi` | `true` lets apps load their page from any `https://` address instead of only from a resource. |

### Templates

The quickest way to start is a ready-made template. Each one is a complete resource with an icon, a finished starter screen and a phone preview that runs in your browser, so you can build the page without joining the server.

{% embed url="<https://github.com/svgAPOLLO/titan-phone-app-template>" %}

| Template          | Stack                                   |
| ----------------- | --------------------------------------- |
| `titan-vanillajs` | HTML, CSS and JavaScript, no build step |
| `titan-reactjs`   | React and Vite                          |
| `titan-reactts`   | React, TypeScript and Vite              |
| `titan-vuejs`     | Vue 3 and Vite                          |
| `titan-svelte`    | Svelte 5 and Vite                       |

Copy the template you like into your resources, rename the folder and follow its README.

### Adding an app

{% stepper %}
{% step %}

#### Register the app

Register the app from a client script, and again on `ttn-phone:client:ready` so it comes back when the phone restarts.

{% code title="your-resource/client.lua" %}

```lua
local function registerApp()
    exports['ttn-phone']:RegisterApp({
        id = 'garagebook',
        name = 'Garage Book',
        description = 'Book a lift at any mechanic in the city.',
        developer = 'Your Studio',
        category = 'vehicles',
        icon = 'icon.svg',
        ui = 'web/index.html',
        tint = '#30D158',
        default = true,
        onMessage = function(data)
            return { ok = true }
        end,
    })
end

AddEventHandler('ttn-phone:client:ready', registerApp)

CreateThread(function()
    while GetResourceState('ttn-phone') ~= 'started' do
        Wait(500)
    end
    registerApp()
end)
```

{% endcode %}
{% endstep %}

{% step %}

#### Add a ui\_page

Your resource needs a `ui_page` so the page can reach your NUI callbacks. An empty HTML page is enough.

{% code title="your-resource/fxmanifest.lua" %}

```lua
ui_page 'blank.html'

files {
    'blank.html',
    'icon.svg',
    'web/**/*',
}

dependency 'ttn-phone'
```

{% endcode %}
{% endstep %}

{% step %}

#### Build the page

Create `web/index.html` and load the page SDK, as shown in The page below. Restart your resource and open the phone.
{% endstep %}
{% endstepper %}

`RegisterApp` returns `true`, or `false` and a reason. Registering the same `id` again updates the app, and apps are removed when the resource that added them stops. Apps can also be registered from the server, which adds them for every player. See the [server exports](https://grp-development.gitbook.io/titan-scripts/resources/titan-phone/exports-and-events/server-exports#custom-apps).

### App fields

| Field           | Type      | Description                                                                      |
| --------------- | --------- | -------------------------------------------------------------------------------- |
| `id`            | string    | Required. Unique id, 2 to 32 characters of `a-z`, `0-9`, `_` or `-`              |
| `name`          | string    | Required. Name under the icon, up to 24 characters                               |
| `ui`            | string    | Required. Page inside your resource, for example `web/index.html`                |
| `icon`          | string    | Square icon inside your resource, or a full link. The phone rounds the corners   |
| `description`   | string    | Text on the App Market page                                                      |
| `developer`     | string    | Shown as "By ..." in the App Market                                              |
| `version`       | string    | Shown on the App Market page                                                     |
| `category`      | string    | App Market group. Default `utilities`                                            |
| `tint`          | string    | Accent colour as `#RRGGBB`                                                       |
| `default`       | boolean   | Install the app on every phone. Default `false`                                  |
| `removable`     | boolean   | Allow players to uninstall it. Default `true`                                    |
| `cover`         | boolean   | `false` opens the app on the inner screen only. Default `true`                   |
| `dark`          | boolean   | Keep the phone's controls dark around the app                                    |
| `sharp`         | boolean   | Draw the page at the phone's real resolution. Default `true`                     |
| `notifications` | boolean   | List the app in the notification settings. Default `true`                        |
| `size`          | number    | Size in MB shown in the App Market                                               |
| `rating`        | number    | Rating shown in the App Market, 0 to 5                                           |
| `screenshots`   | string\[] | Up to 4 images for the App Market                                                |
| `onOpen`        | function  | Called when the player opens the app                                             |
| `onClose`       | function  | Called when the player leaves the app or closes the phone                        |
| `onMessage`     | function  | Receives `TitanPhone.message(data)` from the page. Its return value is sent back |

Categories are `communication`, `productivity`, `media`, `social`, `city`, `vehicles`, `property`, `finance`, `news`, `marketplace`, `work`, `utilities`, `system` and `navigation`.

### The page

Load the page SDK from the phone. It adds `window.TitanPhone`.

{% code title="your-resource/web/index.html" %}

```html
<script src="https://cfx-nui-ttn-phone/html/app-sdk.js"></script>
<script>
  TitanPhone.on("context", (context) => {
    document.getElementById("name").textContent = context.phone.name;
  });

  TitanPhone.on("message", (data) => {
    console.log("from Lua", data);
  });

  TitanPhone.ready();
</script>
```

{% endcode %}

Give `<body>` a background colour, because the phone shows through a transparent page. The SDK sets `data-theme`, `data-screen` and `data-folded` on `<html>`, and the `--phone-tint` CSS variable, so your styles can follow the phone.

{% hint style="info" %}
The page reloads when the player folds or unfolds the phone. Keep state in your resource or in `localStorage`.
{% endhint %}

#### SDK functions

| Function                                | Description                                                                             |
| --------------------------------------- | --------------------------------------------------------------------------------------- |
| `TitanPhone.on(event, handler)`         | Listen to `context`, `ready` or `message`. Returns a function that removes the listener |
| `TitanPhone.off(event, handler)`        | Remove a listener                                                                       |
| `TitanPhone.context`                    | The latest context, `null` until the phone has answered                                 |
| `TitanPhone.ready()`                    | Hide the phone's loading screen once your page is drawn                                 |
| `TitanPhone.fetch(name, data)`          | Call `RegisterNUICallback(name)` in your resource and get its reply                     |
| `TitanPhone.message(data)`              | Send data to your `onMessage` hook and get its return value                             |
| `TitanPhone.notify({ title, message })` | Show a phone notification from your app                                                 |
| `TitanPhone.toast(text)`                | Show a short message at the bottom of the screen                                        |
| `TitanPhone.badge(count)`               | Set the number on your Home Screen icon. `0` clears it                                  |
| `TitanPhone.open(app)`                  | Open another app, for example `Message` or a custom app id                              |
| `TitanPhone.fold(mode)`                 | `fold`, `unfold` or `toggle`                                                            |
| `TitanPhone.close()`                    | Go back to the Home Screen                                                              |

#### Context

| Field                            | Description                                                 |
| -------------------------------- | ----------------------------------------------------------- |
| `id`, `name`, `resource`, `tint` | Your app's details                                          |
| `screen`                         | `cover`, `inner`, or `pane` when the app runs in split view |
| `folded`                         | `true` while the phone is folded                            |
| `theme`                          | `dark` or `light`                                           |
| `language`                       | The phone's language code                                   |
| `phone.name`, `phone.number`     | The player's name and phone number                          |
| `zoom`                           | Scale the page is drawn at                                  |

### Talking to Lua

| From | To                    | How                                 |
| ---- | --------------------- | ----------------------------------- |
| Page | Your NUI callback     | `TitanPhone.fetch('getJobs', data)` |
| Page | Your `onMessage` hook | `TitanPhone.message(data)`          |
| Lua  | Page                  | `SendAppMessage(id, data)`          |
| Lua  | Player                | `SendAppNotification(id, payload)`  |
| Lua  | Home Screen icon      | `SetAppBadge(id, count)`            |

#### Loading data when the app opens

`onOpen` can run before your page has finished loading, so a message sent from `onOpen` can be missed. Let the page ask for its data once it is ready:

```lua
RegisterNUICallback('getJobs', function(_, cb)
    cb(GetAvailableJobs())
end)
```

```js
TitanPhone.on("ready", async () => {
  const jobs = await TitanPhone.fetch("getJobs");
  renderJobs(jobs);
});
```

Use `SendAppMessage` for live updates while the app is open.

### Sharp rendering

The phone is scaled up to fit the player's screen. With `sharp = true`, the phone draws your page at that final scale, so text stays crisp at 1080p, 1440p and 4K. Your layout doesn't change. Size things in `px`, `%`, `em` or `rem`. If your layout depends on `vw` or `vh` units, set `sharp = false`.

### Developing

To see your changes in game without restarting, point `ui` at your dev server, for example `http://localhost:5173`, and set `Config.CustomApps.AllowExternalUi = true` on your development server.

{% hint style="warning" %}
Turn `AllowExternalUi` off again on your live server.
{% endhint %}
