{"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":"creer-une-application-telephonique-personnalisee","status":"publish","type":"post","link":"https:\/\/rpcrate.com\/fr\/build-a-custom-phone-app\/","title":{"rendered":"Cr\u00e9er une petite application de t\u00e9l\u00e9phone FiveM avec 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>Commencez par cr\u00e9er une petite application NUI de type t\u00e9l\u00e9phone pour apprendre l'int\u00e9gralit\u00e9 du cycle de vie : ouverture, message, callback et fermeture. Cet exemple affiche une note de session qui persiste pendant l'ex\u00e9cution de la ressource. Il n'a ni appels, ni service de messagerie, ni base de donn\u00e9es, ni d\u00e9pendance \u00e0 un framework ; les ajouter n\u00e9cessite une autorisation c\u00f4t\u00e9 serveur et une persistance distinctes.<\/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\/fr\/wp-json\/wp\/v2\/posts\/199625","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rpcrate.com\/fr\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rpcrate.com\/fr\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rpcrate.com\/fr\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rpcrate.com\/fr\/wp-json\/wp\/v2\/comments?post=199625"}],"version-history":[{"count":6,"href":"https:\/\/rpcrate.com\/fr\/wp-json\/wp\/v2\/posts\/199625\/revisions"}],"predecessor-version":[{"id":217336,"href":"https:\/\/rpcrate.com\/fr\/wp-json\/wp\/v2\/posts\/199625\/revisions\/217336"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rpcrate.com\/fr\/wp-json\/wp\/v2\/media\/199627"}],"wp:attachment":[{"href":"https:\/\/rpcrate.com\/fr\/wp-json\/wp\/v2\/media?parent=199625"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rpcrate.com\/fr\/wp-json\/wp\/v2\/categories?post=199625"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rpcrate.com\/fr\/wp-json\/wp\/v2\/tags?post=199625"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}