Skip to content

App Development Guide

How to create, register, and deploy apps for the wtf_group tablet.

App Structure

Every tablet app is a React component in ui/src/apps/<AppName>/index.jsx.

ui/src/apps/
├── MyNewApp/
│   └── index.jsx       # Main app component

Minimal App

jsx
import { useTablet } from '../../context/TabletContext';

export default function MyNewApp() {
    const { themeColors } = useTablet();

    return (
        <div style={{ height: '100%', padding: 16 }}>
            <h1 style={{ color: themeColors.text }}>My App</h1>
            <p style={{ color: themeColors.textDim }}>Hello world</p>
        </div>
    );
}

Registering an App

Method 1: App Registry (Static)

Add to ui/src/lib/appRegistry.js:

js
import { GroupsIcon, CalculatorIcon } from './icons';

export const APP_REGISTRY = {
    // ... existing apps ...
    my_new_app: {
        id: 'my_new_app',
        name: 'My App',
        description: 'Description of my app',
        iconComponent: CalculatorIcon,  // SVG component from icons.jsx
        iconText: '🚀',                 // Fallback emoji (if no iconComponent)
        color: '#ff6600',
        category: 'tools',              // system, tools, productivity, internet, media, info, developer
        builtin: true,                  // true = always installed, false = downloadable
        downloadable: true,             // true = shows in App Store
        showOnDesktop: true,            // true = shows desktop icon
        singleton: true,                // true = only one window allowed
        defaultWidth: 600,
        defaultHeight: 400,
        minWidth: 300,
        minHeight: 200,
    },
};

Then add the component mapping in App.jsx:

js
import MyNewApp from './apps/MyNewApp';

const APP_COMPONENTS = {
    // ... existing ...
    my_new_app: MyNewApp,
};

Method 2: Lua Registration (Dynamic)

From another resource:

lua
-- client.lua
exports['wtf_group']:RegisterApp({
    id = 'my_external_app',
    name = 'External App',
    icon = 'fas fa-puzzle-piece',
    component = 'MyExternalApp',
    category = 'custom',
    color = '#ff6600',
    builtin = false,
    showOnDesktop = true,
    singleton = true,
    defaultWidth = 600,
    defaultHeight = 400,
})

Using Settings

All apps share the centralized settings via useTablet():

jsx
import { useTablet } from '../../context/TabletContext';

function MyApp() {
    const {
        themeColors,    // Derived colors (accent, text, surface, border, etc.)
        accentColor,    // Raw accent hex
        fontFamily,     // CSS font-family string
        fontSize,       // Base font size in px
        uiScale,        // UI scale percentage (100 = normal)
        borderRadius,   // Border radius in px
        showAnimations, // Animation toggle
        showGlowEffects,// Glow effects toggle
    } = useTablet();

    return (
        <div style={{
            background: themeColors.windowBg,
            color: themeColors.text,
            fontFamily: fontFamily,
            borderRadius: borderRadius,
        }}>
            {/* Your app content */}
        </div>
    );
}

Persisting App Data

Use dbStorage.js to save data to MySQL:

jsx
import { useState, useEffect } from 'react';
import { loadAppDataFromDB, saveAppDataToDB } from '../../lib/dbStorage';

function MyApp() {
    const [data, setData] = useState([]);

    // Load from DB on mount
    useEffect(() => {
        loadAppDataFromDB('my_app', 'notes').then(saved => {
            if (saved && Array.isArray(saved)) setData(saved);
        }).catch(() => {});
    }, []);

    // Save to DB on change
    const save = (newData) => {
        setData(newData);
        saveAppDataToDB('my_app', 'notes', newData);
    };

    return <div>...</div>;
}

Storage keys:

  • appId — Your app's ID (e.g., 'my_app')
  • key — Data key (e.g., 'notes', 'settings', 'bookmarks')
  • value — Any JSON-serializable data

App Store (Downloadable Apps)

For apps that players can install/uninstall:

  1. Set builtin: false and downloadable: true in registry
  2. Add to COMPONENT_MAP in AppStore/index.jsx
  3. The App Store handles install/uninstall lifecycle

VPN Shield App (Built-in Downloadable)

The VPN app is a downloadable app that provides encrypted proxy connections.

Registry entry (lib/appRegistry.js):

js
vpn: {
    id: 'vpn',
    name: 'VPN Shield',
    description: 'Encrypted proxy connection for anonymous browsing',
    iconComponent: VpnIcon,
    color: '#388e3c',
    category: 'darkweb',         // Darkweb category — gated by VPN
    darkweb: true,               // Requires darkweb VPN proxy to install
    downloadable: true,
    showOnDesktop: true,
    singleton: true,
    defaultWidth: 420,
    defaultHeight: 520,
}

Server-side (server/vpn.lua):

  • VpnMgr.GetProxies(source) — loads proxies + connection state + keys
  • VpnMgr.Connect(source, proxyId) — connects to proxy
  • VpnMgr.Disconnect(source) — disconnects
  • VpnMgr.ActivateKey(source, keyCode) — activates license key
  • Exports: IsVpnConnected, IsPlayerConnectedToProxy, GetPlayerActiveProxy

Client-side (client/vpn.lua):

  • 4 NUI callbacks forwarding to server events
  • Receives vpnData event and forwards to React via SendNUIMessage

Data storage (in wtf_tablet_app_data):

  • app_id='vpn', data_key='connection'{ proxy_id, proxy_name, connected, expires }
  • app_id='vpn', data_key='keys'[{ key_code, proxy_id, permanent, expires }, ...]

Full documentation: VPN Shield App

Window Props

Apps can receive props from the window:

jsx
function MyApp({初始Data, mode }) {
    // Props passed via openApp('my_app', { props: { initialData: '...' } })
}

Update props from inside the app:

jsx
import { useApp } from '../../context/AppContext';

function MyApp() {
    const { updateWindowProps } = useApp();
    const windowId = useApp().windows.find(w => w.appId === 'my_app')?.id;

    // Update props
    updateWindowProps(windowId, { mode: 'edit' });
}

Sound Effects

jsx
import { Sounds } from '../../lib/sounds';

Sounds.click();      // Button click
Sounds.hover();      // Mouse hover
Sounds.open();       // App/window open
Sounds.close();      // App/window close
Sounds.minimize();   // Window minimize
Sounds.maximize();   // Window maximize
Sounds.notify();     // Notification received
Sounds.error();      // Error occurred
Sounds.success();    // Success action
Sounds.tick();       // Subtle interaction
Sounds.type();       // Typing

SVG Icons

Create custom icons in ui/src/lib/icons.jsx:

jsx
export function MyIcon({ size = 16, color = 'currentColor' }) {
    return (
        <svg width={size} height={size} viewBox="0 0 24 24"
             fill="none" stroke={color} strokeWidth="2"
             strokeLinecap="round" strokeLinejoin="round">
            <path d="M12 2L2 7l10 5 10-5-10-5z" />
            <path d="M2 17l10 5 10-5" />
            <path d="M2 12l10 5 10-5" />
        </svg>
    );
}

Then reference in registry:

js
import { MyIcon } from '../lib/icons';
// ...
iconComponent: MyIcon,

Best Practices

  1. Always use useTablet() for theme colors — never hardcode colors
  2. Use themeColors propertieswindowBg, surface, text, textDim, accent, border, etc.
  3. Save data to DB — use saveAppDataToDB() for anything persistent
  4. Load from DB on mount — use loadAppDataFromDB() in useEffect
  5. Use Sounds.* for audio feedback
  6. Use SVG icons — not font-awesome or images
  7. Set proper minWidth/minHeight — prevent windows from being too small
  8. Use singleton: true for most apps — prevents duplicate windows
  9. Keep apps self-contained — each app should work independently

AIFAZI — FiveM Resources