Votre propre application dessus
Un exemple fonctionnel se trouve dans example/mic_phone_demo. Copiez le dossier, modifiez les noms, et vous avez une application.
my_app/ fxmanifest.lua client.lua registers the app server.lua anything that touches money or the database ui/index.html your page ui/icon.svg your icon-- fxmanifest.luafx_version 'cerulean'game 'gta5'
client_script 'client.lua'server_script 'server.lua'
files { -- WITHOUT THIS your page and icon are not served and the frame comes up blank 'ui/index.html', 'ui/icon.svg',}Le bloc files est lâĂ©lĂ©ment que tout le monde oublie. FiveM ne sert que ce quâune ressource dĂ©clare dans cette liste, donc une page qui nây figure pas rĂ©pond par une 404 et votre application sâouvre sur un cadre blanc.
local function register() exports.mic_phone:AddApp({ id = 'phonedemo', name = 'Field Notes', icon = ('https://cfx-nui-%s/ui/icon.svg'):format(GetCurrentResourceName()), ui = ('%s/ui/index.html'):format(GetCurrentResourceName()), })end
AddEventHandler('mic_phone:ready', register) -- fires on start, and after every restart of the phoneCâest lâintĂ©gralitĂ© du contrat, et il nây a rien de plus : rien nâest configurĂ© sur le tĂ©lĂ©phone, et rien nâest redĂ©marrĂ©. DĂ©marrez votre ressource et lâapplication apparaĂźt sur lâĂ©cran dâaccueil â default = true la place directement lĂ , default = false la place dans lâApp Store. Dans les deux cas, la dĂ©cision vous appartient, dans votre propre fichier.
Config.AppList.Custom existe pour quâun propriĂ©taire de serveur puisse prĂ©valoir sur vos choix â tarifer votre application, lâoffrir prĂ©installĂ©e ou la dĂ©sactiver. Il nâest jamais requis pour que votre application fonctionne.
Quatre contraintes que vous nâavez pas Ă subir, contrairement Ă dâautres tĂ©lĂ©phones :
- Vous nâavez pas Ă supprimer lâapplication avant de la rajouter. Enregistrer le mĂȘme ID depuis la mĂȘme ressource la met Ă jour. Seule une ressource diffĂ©rente revendiquant un ID dĂ©jĂ attribuĂ© est refusĂ©e, avec indication du motif.
- Vous nâavez pas Ă attendre le tĂ©lĂ©phone.
mic_phone:readyse dĂ©clenche lorsquâil est prĂȘt Ă recevoir des applications et Ă nouveau aprĂšs chaque redĂ©marrage, votre application revient donc dâelle-mĂȘme aprĂšs unrestart mic_phone. - Vous nâavez pas Ă nettoyer lors de lâarrĂȘt de votre ressource. Vos applications partent avec elle. Un script arrĂȘtĂ© ne laisse jamais derriĂšre lui une icĂŽne inactive.
- Vous nâavez pas Ă redĂ©marrer le tĂ©lĂ©phone. Une application enregistrĂ©e pendant quâun joueur tient son tĂ©lĂ©phone en main apparaĂźt dessus instantanĂ©ment.
Ce quâune application peut dĂ©clarer sur elle-mĂȘme
Section intitulĂ©e « Ce quâune application peut dĂ©clarer sur elle-mĂȘme »id | lettres, chiffres, _ et -. Unique sur lâensemble du serveur |
name | nom affichĂ© sous lâicĂŽne |
subtitle, description, features, version | la page sur lâApp Store |
developer, category | affichĂ©s dans lâApp Store |
color | couleur de fond de lâicĂŽne lorsquâaucune image icon nâest fournie |
icon | une URL. https://cfx-nui-<resource>/path.png la sert depuis votre propre dossier |
ui | resource/path/index.html, la page de votre application. Ă omettre pour une app 100 % Lua. Un chemin interne Ă une ressource de ce serveur, jamais une URL externe â voir ci-dessous |
landscape | indique que la page nécessite le téléphone déplié en tablette |
size, images | Ă©lĂ©ments affichĂ©s sur la page App Store. Les images doivent ĂȘtre en https:// |
default | true la dĂ©pose directement sur lâĂ©cran dâaccueil au lieu de lâApp Store |
onOpen, onClose | fonctions appelĂ©es Ă lâouverture et Ă la fermeture |
Vous ne pouvez pas fixer votre propre prix. Il nây a pas de champ price, et câest volontaire : cette valeur rĂ©siderait cĂŽtĂ© client, et un prix dĂ©clarĂ© par le client est un prix manipulable par le client â lâApp Store afficherait un montant et le serveur ne prĂ©lĂšverait rien. La tarification est du ressort de Config.AppList.Custom, Ă©crit par le propriĂ©taire du serveur et lu par le serveur. Si vous souhaitez que votre application soit payante, demandez-lui dâajouter une ligne ; cela ne prend quâune ligne.
La ville a le dernier mot. Tout ce que vous demandez ici constitue une suggestion. Config.AppList.Custom dans la configuration du tĂ©lĂ©phone peut tarifer votre application, la distribuer prĂ©installĂ©e ou la dĂ©sactiver totalement, sans que personne nâait Ă modifier votre ressource. Câest dĂ©libĂ©rĂ© â un gestionnaire de serveur ne devrait pas avoir Ă forker un script pour le faire payer.
Communiquer avec votre page
Section intitulĂ©e « Communiquer avec votre page »Votre page est une iframe. Elle ne peut pas pĂ©nĂ©trer dans le tĂ©lĂ©phone, lire les messages ni dessiner hors de son rectangle ; le tĂ©lĂ©phone ne peut pas non plus pĂ©nĂ©trer dans votre iframe. Tout sâĂ©change par messages.
ui doit ĂȘtre un chemin vers une ressource situĂ©e sur ce serveur. Une frame a besoin que la Content-Security-Policy de la page autorise son origine, et les CSP ne permettent pas dâĂ©crire ânâimporte quelle ressource cfx-nui-â â les jokers ne sont autorisĂ©s que sur le sous-domaine le plus Ă gauche, donc https://cfx-nui-* ne peut pas ĂȘtre formulĂ© et la directive doit ĂȘtre ouverte Ă https: pour que votre frame puisse charger. Câest vaste, alors le tĂ©lĂ©phone restreint cela de lâautre cĂŽtĂ© : il construit lui-mĂȘme lâorigine dâaprĂšs le chemin de votre ressource et rejette tout ce qui se prĂ©sente dĂ©jĂ comme une URL. Si vous souhaitez hĂ©berger votre page, faites-le depuis votre propre ressource.
Dans votre page, deux lignes :
parent.postMessage({ type: 'phone:ready' }, '*'); // say you have loadedconst send = m => parent.postMessage({ type: 'phone:message', message: m }, '*');et écoutez :
window.addEventListener('message', e => { if (e.data?.type === 'phone:theme') document.documentElement.dataset.theme = e.data.theme; if (e.data?.type === 'phone:message') { /* from your Lua */ }});Dans votre script Lua :
exports.mic_phone:SendAppMessage('phonedemo', { kind = 'hello' })
AddEventHandler(GetCurrentResourceName() .. ':phone:message', function(message) -- from your page. Routed here by the resource that owns the app, so nothing else can send theseend)Envoyez ce que vous voulez depuis onOpen. Un message destinĂ© Ă une page qui nâa pas encore fini de charger attend sagement et arrive dans lâordre dĂšs que celle-ci Ă©met phone:ready. Dâautres tĂ©lĂ©phones dĂ©conseillent cette pratique car leurs messages entrent en concurrence avec le chargement de la frame ; ici ce nâest pas le cas, car envoyer des donnĂ©es Ă lâouverture de lâapplication est la dĂ©marche la plus naturelle qui soit, et ce qui est naturel doit fonctionner.
phone:theme est transmis avec le mode light/dark du téléphone et sa langue lorsque votre page signale sa disponibilité. Suivez ces valeurs et votre application cessera de ressembler à un site web plaqué artificiellement.
Demander un service au téléphone
Section intitulĂ©e « Demander un service au tĂ©lĂ©phone »Votre page peut demander au tĂ©lĂ©phone dâexĂ©cuter des actions quâil maĂźtrise dĂ©jĂ â afficher une notification, ouvrir lâappareil photo, faire choisir une photo ou un contact. Quinze lignes initialisent le systĂšme, puis chaque requĂȘte se rĂ©sume Ă un simple await :
let next = 1; const waiting = new Map();const ask = (what, args) => new Promise(resolve => { const id = next++; waiting.set(id, resolve); parent.postMessage({ type: 'phone:ask', id, what, with: args || {} }, '*');});window.addEventListener('message', e => { if (e.data?.type === 'phone:answer') { const done = waiting.get(e.data.id); if (done) { waiting.delete(e.data.id); done(e.data.value); } }});const me = await ask('me'); // { number, name }const photo = await ask('photos.pick'); // { id, url, width, height } or nullconst shot = await ask('camera.take'); // the same, from the real Cameraconst person = await ask('contacts.pick'); // { name, number } or nullawait ask('notify', { title: 'Field Notes', body: 'Something happened.' });await ask('toast', { text: 'Saved.' });const look = await ask('theme'); // { theme, language, tablet }await ask('close'); // put yourself away| Demande | Ce qui est renvoyé | |
|---|---|---|
me | { number, name } | ce que les autres tĂ©lĂ©phones voient dĂ©jĂ , et rien dâautre sur la personne |
notify | true | classé sous votre application, ainsi la mettre en sourdine dans les ParamÚtres vous met en sourdine |
toast | true | un bandeau discret en bas de lâĂ©cran |
photos.pick | une photo, ou null | ouvre la galerie du téléphone ; null signifie fermeture sans choix |
camera.take | une photo, ou null | ouvre lâAppareil photo en plein jeu, attend et vous ramĂšne sur lâapp |
contacts.pick | un contact, ou null | ouvre un volet de sélection de contacts |
theme | { theme, language, tablet } | les mĂȘmes valeurs que celles poussĂ©es par phone:theme |
close | true | revient Ă lâĂ©cran dâaccueil |
Et le volet qui permet dâinteragir activement avec le tĂ©lĂ©phone :
await ask('message.compose', { number: '555-0110', text: 'Your order is ready.' });await ask('call.start', { number: '555-0110' });await ask('share.text', { text: 'Look at this.' }); // â { to, number } or nullawait ask('share.photo', { url: photo.url });await ask('photos.save', { url: someImageUrl }); // â { id, width, height } or nullawait ask('waypoint.set', { x: 219, y: -855 });const money = await ask('money.balance'); // { balance }| Demande | Ce qui est renvoyĂ© | |
|---|---|---|
message.compose | true | ouvre Messages avec le texte prérempli. Un joueur appuie sur envoyer |
call.start | true | compose et appelle le numéro |
share.text / share.photo | { to, number }, ou null | ouvre un sĂ©lecteur, puis prĂ©pare lâenvoi vers le destinataire choisi |
photos.save | { id, width, height } | enregistre une image générée par votre app dans Photos |
waypoint.set | true | place un repĂšre GPS sur la carte |
money.balance | { balance } | lecture seule â lâargent se dĂ©place cĂŽtĂ© serveur, voir ci-dessous |
Rien nâest jamais envoyĂ© au nom dâun joueur Ă son insu. message.compose remplit le champ de saisie et confie le bouton dâenvoi Ă lâhumain ; le partage demande systĂ©matiquement le destinataire. Une application capable dâenvoyer des SMS Ă vos contacts en secret serait bien pire quâune application dĂ©pourvue dâenvoi de messages.
Lâargent est manipulĂ© cĂŽtĂ© serveur, depuis votre propre script serveur, car un prix contestable par un client nâest pas un prix. Trois fonctions sont proposĂ©es, selon lâentitĂ© ayant rĂ©alisĂ© le mouvement :
-- somebody else already took the money; this only writes the line in the Walletexports.mic_phone:AddTransaction(src, { title = 'Tequi-la-la', detail = 'Two drinks', amount = -40 })
-- take it, and write the line. false, 'funds' and nothing moved when they cannot paylocal ok, tx = exports.mic_phone:Charge(src, 250, { title = 'Field Notes', detail = 'Pro upgrade' })
-- give it, and write the lineexports.mic_phone:Pay(src, 1000, { title = 'Delivery', detail = 'Job finished' })
exports.mic_phone:GetBalance(src)Toutes trois mettent Ă jour le Portefeuille Ă lâĂ©cran instantanĂ©ment. Charge et Pay rĂ©ussissent intĂ©gralement ou Ă©chouent intĂ©gralement : un prĂ©lĂšvement refusĂ© laisse le solde et lâhistorique dans leur Ă©tat dâorigine.
Deux principes fondamentaux justifient la concision de cette API :
Votre application ne reçoit jamais de donnĂ©es quâon ne lui a pas explicitement donnĂ©es. contacts.pick ne renvoie pas le carnet dâadresses â il ouvre un volet, le joueur sĂ©lectionne un nom, et seul ce nom est renvoyĂ©. Interroger en boucle ne permet pas de siphonner le rĂ©pertoire ; chaque rĂ©ponse requiert une action utilisateur. Il en va de mĂȘme pour les photos.
Seule lâapplication Ă lâĂ©cran a le droit de poser une question. Une demande provenant dâune frame qui nâest pas lâapplication actuellement ouverte nâobtient aucune rĂ©ponse et nâouvre rien ; une page laissĂ©e en arriĂšre-plan ne peut donc pas agir pendant que le joueur fait autre chose.
Si ce dont vous avez besoin ne figure pas dans cette liste, ce nâest pas un refus du tĂ©lĂ©phone â câest simplement que personne ne lâa encore ajoutĂ©. En ajouter une consiste en une entrĂ©e dans PHONE_APP_ASKS au sein de html/apps/store/app-api.js, rien de plus.
Se renseigner sur le téléphone
Section intitulĂ©e « Se renseigner sur le tĂ©lĂ©phone »exports.mic_phone:RegisteredApps() --> every app registered on this client, and by whomexports.mic_phone:IsAppOpen('phonedemo') --> is it on screenexports.mic_phone:OpenAppId() --> which custom app is, or nilexports.mic_phone:OpenApp('phonedemo') --> open it, if the phone will open at allRegisteredApps est lâexport Ă consulter lorsque votre application nâapparaĂźt pas. La commande serveur phoneapps affiche ce que prĂ©voit la configuration, ce qui reprĂ©sente une liste diffĂ©rente â votre application rĂ©side sur le client qui lâa enregistrĂ©e, et nâapparaĂźt dans phoneapps que si une entrĂ©e lui a Ă©tĂ© consacrĂ©e dans Config.AppList.Custom.
Une application qui nâest ni en default = true ni marquĂ©e comme installed dans la configuration attend dans lâApp Store que quelquâun la tĂ©lĂ©charge. Si la vĂŽtre est bien enregistrĂ©e mais introuvable sur lâĂ©cran dâaccueil, câen est gĂ©nĂ©ralement la cause.
En cas de dysfonctionnement
Section intitulée « En cas de dysfonctionnement »| Ce que vous constatez | Presque toujours |
|---|---|
| Lâapplication nâest nulle part | Consultez dâabord RegisteredApps(). Si elle nây figure pas, AddApp a Ă©tĂ© rejetĂ© â la raison est inscrite dans la console client (F8), et non serveur |
| Elle est enregistrĂ©e mais absente de lâaccueil | Elle se trouve dans lâApp Store : vous avez dĂ©fini default = false, ou le gestionnaire a mis How = 'store' dans Config.AppList.Custom |
| Lâapplication sâouvre sur une page blanche | Votre fichier ui nâest pas rĂ©pertoriĂ© dans le bloc files de votre fxmanifest |
Refused to frame dans la console | Votre ui est une URL. Ce doit ĂȘtre un chemin vers une ressource situĂ©e sur ce serveur |
| La page charge, mais les messages nâarrivent pas | Votre page nâa jamais envoyĂ© phone:ready. Rien nâest dĂ©livrĂ© tant quâelle ne lâa pas fait â les messages patientent et arrivent tous Ă lâenvoi de ce signal |
no answer in 12000ms | Vous avez sollicité une action inexistante sur le téléphone. Vérifiez le nom avec la liste ci-dessus |
phoneapps sur le serveur affiche la configuration et non votre application â une application enregistrĂ©e par une ressource vit sur le client qui lâa dĂ©clarĂ©e, et nây apparaĂźt que si une entrĂ©e Config.AppList.Custom existe. exports.mic_phone:RegisteredApps() fournit la liste rĂ©ellement prĂ©sente sur cette machine.
Le reste de lâAPI du tĂ©lĂ©phone â state bags, appels, contacts, notifications â est disponible sur la page Exports et state bags.