{"id":199625,"date":"2025-10-04T16:06:10","date_gmt":"2025-10-04T14:06:10","guid":{"rendered":"https:\/\/fivemx.com\/?p=199625"},"modified":"2026-08-16T13:45:15","modified_gmt":"2026-08-16T11:45:15","slug":"crea-una-aplicacion-de-telefono-personalizada","status":"publish","type":"post","link":"https:\/\/rpcrate.com\/es\/build-a-custom-phone-app\/","title":{"rendered":"Crea una peque\u00f1a aplicaci\u00f3n de tel\u00e9fono FiveM con NUI"},"content":{"rendered":"<p>Build a small phone-style NUI app first to learn the complete open, message, callback and close lifecycle. This example shows a session note that stays while the resource runs. It has no calls, messaging service, database or framework dependency; adding those requires separate server-side authorization and persistence.<\/p>\n<h2 id=\"create-three-files\">Create three files<\/h2>\n<p>Create <code data-no-translation=\"\" translate=\"no\">resources\/[local]\/tutorial_phone\/<\/code> with <code data-no-translation=\"\" translate=\"no\">fxmanifest.lua<\/code>, <code data-no-translation=\"\" translate=\"no\">client.lua<\/code> and <code data-no-translation=\"\" translate=\"no\">index.html<\/code>. The example targets the GTA V Legacy <code data-no-translation=\"\" translate=\"no\">gta5<\/code> resource runtime. Check Cfx.re\u2019s Enhanced migration guidance before adapting it to an Enhanced deployment.<\/p>\n<p><code data-no-translation=\"\" translate=\"no\">fxmanifest.lua<\/code>:<\/p>\n<pre data-no-translation=\"\" translate=\"no\"><code data-no-translation=\"\" translate=\"no\">fx_version 'cerulean'\ngame 'gta5'\nui_page 'index.html'\nfiles { 'index.html' }\nclient_script 'client.lua'<\/code><\/pre>\n<p><code data-no-translation=\"\" translate=\"no\">client.lua<\/code>:<\/p>\n<pre data-no-translation=\"\" translate=\"no\"><code data-no-translation=\"\" translate=\"no\">local visible = false\nlocal ready = false\n\nlocal function closePhone()\n    visible = false\n    SetNuiFocus(false, false)\n    SendNUIMessage({ action = 'close' })\nend\n\nRegisterCommand('tutorial_phone', function()\n    if not ready then\n        print('Phone UI is loading; try again shortly.')\n        return\n    end\n    if visible then closePhone(); return end\n    visible = true\n    SetNuiFocus(true, true)\n    SendNUIMessage({ action = 'open' })\nend, false)\n\nRegisterNUICallback('ready', function(_, cb)\n    ready = true\n    cb({ ok = true })\nend)\n\nRegisterNUICallback('close', function(_, cb)\n    closePhone()\n    cb({ ok = true })\nend)\n\nAddEventHandler('onClientResourceStop', function(name)\n    if name == GetCurrentResourceName() and visible then\n        SetNuiFocus(false, false)\n    end\nend)<\/code><\/pre>\n<p><code data-no-translation=\"\" translate=\"no\">index.html<\/code>:<\/p>\n<pre data-no-translation=\"\" translate=\"no\"><code data-no-translation=\"\" translate=\"no\">&lt;!doctype html&gt;\n&lt;html lang=\"en\"&gt;\n&lt;head&gt;\n&lt;meta charset=\"utf-8\"&gt;\n&lt;meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"&gt;\n&lt;title&gt;Session note&lt;\/title&gt;\n&lt;style&gt;\nbody { margin: 0; background: transparent; font: 18px system-ui; }\n#phone { position: fixed; right: 4vw; bottom: 4vh; width: min(320px, 85vw);\n  padding: 24px; box-sizing: border-box; color: #fff; background: #18222d;\n  border-radius: 20px; }\n[hidden] { display: none !important; }\ntextarea { box-sizing: border-box; width: 100%; min-height: 140px; font: inherit;\n  margin: 12px 0; padding: 8px; }\nbutton { font: inherit; padding: 10px 16px; cursor: pointer; }\n:focus-visible { outline: 3px solid #67d7ff; outline-offset: 3px; }\n&lt;\/style&gt;\n&lt;\/head&gt;\n&lt;body&gt;\n&lt;section id=\"phone\" role=\"dialog\" aria-label=\"Session note\" hidden&gt;\n  &lt;h1&gt;Session note&lt;\/h1&gt;\n  &lt;label for=\"note\"&gt;What will your character do next?&lt;\/label&gt;\n  &lt;textarea id=\"note\" maxlength=\"500\"&gt;&lt;\/textarea&gt;\n  &lt;p&gt;This note clears when the resource restarts.&lt;\/p&gt;\n  &lt;button id=\"close\" type=\"button\"&gt;Close&lt;\/button&gt;\n  &lt;p id=\"error\" role=\"status\"&gt;&lt;\/p&gt;\n&lt;\/section&gt;\n&lt;script&gt;\nconst phone = document.getElementById('phone');\nconst note = document.getElementById('note');\nconst error = document.getElementById('error');\nasync function call(name) {\n  const response = await fetch(`https:\/\/${GetParentResourceName()}\/${name}`, {\n    method: 'POST', headers: { 'Content-Type': 'application\/json' }, body: '{}'\n  });\n  if (!response.ok) throw new Error('NUI callback failed');\n  return response.json();\n}\nwindow.addEventListener('message', ({ data }) =&gt; {\n  if (data?.action === 'open') {\n    phone.hidden = false; error.textContent = ''; note.focus();\n  } else if (data?.action === 'close') {\n    phone.hidden = true;\n  }\n});\nasync function close() {\n  try { await call('close'); }\n  catch { error.textContent = 'Could not close. Check the F8 console.'; }\n}\ndocument.getElementById('close').addEventListener('click', close);\nwindow.addEventListener('keydown', event =&gt; {\n  if (event.key === 'Escape' &amp;&amp; !phone.hidden) { event.preventDefault(); close(); }\n});\ncall('ready').catch(() =&gt; console.error('Phone readiness callback failed.'));\n&lt;\/script&gt;\n&lt;\/body&gt;\n&lt;\/html&gt;<\/code><\/pre>\n<h2 id=\"start-and-verify\">Start and verify<\/h2>\n<p>Add <code data-no-translation=\"\" translate=\"no\">ensure tutorial_phone<\/code> to the active server configuration. On your test server, run <code data-no-translation=\"\" translate=\"no\">refresh<\/code> and <code data-no-translation=\"\" translate=\"no\">ensure tutorial_phone<\/code>, connect, then run <code data-no-translation=\"\" translate=\"no\">tutorial_phone<\/code> in F8. The app should open and focus its note field.<\/p>\n<p>Type a note, close with the button and reopen. The note should remain for this resource session. Test Escape, repeated open\/close and stopping the resource while open; game input should be released. Reconnecting or restarting the resource creates a new UI session and clears the note.<\/p>\n<h2 id=\"inspect-the-nui-boundary\">Inspect the NUI boundary<\/h2>\n<p>The browser calls the resource callback by its actual parent name. Lua always replies through <code data-no-translation=\"\" translate=\"no\">cb<\/code>, and the browser handles a failed request. Follow <a href=\"https:\/\/docs.fivem.net\/docs\/scripting-manual\/nui-development\/full-screen-nui\/\">Cfx.re\u2019s NUI debugging instructions<\/a> to inspect console and callback requests on your client.<\/p>\n<h2 id=\"add-real-phone-features-deliberately\">Add real phone features deliberately<\/h2>\n<p>An existing phone resource may expose its own app SDK; use that supported integration instead of running a second full phone. Persistent contacts need character ownership checks on the server, validated lengths, parameterized queries and a migration for the installed schema.<\/p>\n<p>Real calls need the chosen voice resource\u2019s supported integration. Never accept an arbitrary character identifier from the UI as proof of access, or claim a contacts form is a complete secure phone system.<\/p>\n<h2 id=\"reference-documentation\">Reference documentation<\/h2>\n<p><a href=\"https:\/\/docs.fivem.net\/docs\/scripting-manual\/nui-development\/nui-callbacks\/\">Cfx.re \u2014 nui callbacks<\/a> \u00b7 <a href=\"https:\/\/docs.fivem.net\/docs\/scripting-reference\/resource-manifest\/\">Cfx.re \u2014 resource manifest<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Crea primero una peque\u00f1a aplicaci\u00f3n NUI estilo tel\u00e9fono para aprender el ciclo completo de abrir, enviar mensajes, ejecutar callbacks y cerrar. Este ejemplo muestra una nota de sesi\u00f3n que permanece mientras el recurso se ejecuta. No tiene llamadas, servicio de mensajer\u00eda, base de datos o dependencia de framework; a\u00f1adir eso requiere autorizaci\u00f3n y persistencia separadas del lado del servidor.<\/p>","protected":false},"author":1,"featured_media":199627,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2340,1899],"tags":[3001],"class_list":["post-199625","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-lua-scripting","category-tutorials","tag-fivem-script"],"_links":{"self":[{"href":"https:\/\/rpcrate.com\/es\/wp-json\/wp\/v2\/posts\/199625","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rpcrate.com\/es\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rpcrate.com\/es\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rpcrate.com\/es\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rpcrate.com\/es\/wp-json\/wp\/v2\/comments?post=199625"}],"version-history":[{"count":6,"href":"https:\/\/rpcrate.com\/es\/wp-json\/wp\/v2\/posts\/199625\/revisions"}],"predecessor-version":[{"id":217336,"href":"https:\/\/rpcrate.com\/es\/wp-json\/wp\/v2\/posts\/199625\/revisions\/217336"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rpcrate.com\/es\/wp-json\/wp\/v2\/media\/199627"}],"wp:attachment":[{"href":"https:\/\/rpcrate.com\/es\/wp-json\/wp\/v2\/media?parent=199625"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rpcrate.com\/es\/wp-json\/wp\/v2\/categories?post=199625"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rpcrate.com\/es\/wp-json\/wp\/v2\/tags?post=199625"}],"curies":[{"name":"gracias","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}