{"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":"erstellen-sie-eine-benutzerdefinierte-telefon-app","status":"publish","type":"post","link":"https:\/\/rpcrate.com\/de\/build-a-custom-phone-app\/","title":{"rendered":"Eine kleine FiveM-Telefon-App mit NUI entwickeln"},"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>Entwickle zun\u00e4chst eine kleine telefon\u00e4hnliche NUI-App, um den vollst\u00e4ndigen Ablauf aus \u00d6ffnen, Nachrichten, Callbacks und Schlie\u00dfen zu lernen. Dieses Beispiel zeigt eine Sitzungsnotiz, die w\u00e4hrend der Ressourcenlaufzeit erhalten bleibt. Es enth\u00e4lt weder Anrufe noch Nachrichtendienst, Datenbank oder Framework-Abh\u00e4ngigkeit; daf\u00fcr brauchst du zus\u00e4tzliche serverseitige Berechtigungspr\u00fcfungen und dauerhafte Speicherung.<\/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\/de\/wp-json\/wp\/v2\/posts\/199625","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rpcrate.com\/de\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rpcrate.com\/de\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rpcrate.com\/de\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rpcrate.com\/de\/wp-json\/wp\/v2\/comments?post=199625"}],"version-history":[{"count":6,"href":"https:\/\/rpcrate.com\/de\/wp-json\/wp\/v2\/posts\/199625\/revisions"}],"predecessor-version":[{"id":217336,"href":"https:\/\/rpcrate.com\/de\/wp-json\/wp\/v2\/posts\/199625\/revisions\/217336"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rpcrate.com\/de\/wp-json\/wp\/v2\/media\/199627"}],"wp:attachment":[{"href":"https:\/\/rpcrate.com\/de\/wp-json\/wp\/v2\/media?parent=199625"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rpcrate.com\/de\/wp-json\/wp\/v2\/categories?post=199625"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rpcrate.com\/de\/wp-json\/wp\/v2\/tags?post=199625"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}