O seu próprio app nele
Um exemplo funcional está em example/mic_phone_demo. Copie a pasta, mude os nomes e você terá um aplicativo.
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',}O bloco files é a única coisa que as pessoas esquecem. O FiveM só serve o que um recurso lista ali, então uma página que não esteja nessa lista responde 404 e o seu aplicativo abre em branco.
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 phoneEsse é o contrato inteiro, e é tudo: nada é configurado no telefone e nada é reiniciado. Inicie o seu recurso e o aplicativo estará na Tela de Início — default = true o coloca lá, default = false o coloca na App Store. De qualquer forma, a decisão é sua, no seu próprio arquivo.
Config.AppList.Custom existe para que o dono do servidor possa sobrepor a sua escolha — colocar preço no seu aplicativo, entregá-lo pré-instalado ou desativá-lo. Isso nunca é necessário para que o seu aplicativo funcione.
Quatro coisas que você não precisa fazer, mas que outros telefones obrigam:
- Você não remove o aplicativo antes de adicioná-lo. Registrar o mesmo ID a partir do mesmo recurso apenas o atualiza. Somente um recurso diferente tentando reivindicar um ID que já pertence a outro é recusado, e informado do motivo.
- Você não precisa esperar pelo telefone.
mic_phone:readydispara quando ele está pronto para receber aplicativos e novamente toda vez que ele reinicia, então o seu aplicativo volta sozinho após umrestart mic_phone. - Você não precisa limpar nada quando o seu recurso para. Seus aplicativos vão embora com ele. Um script parado nunca deixa para trás um ícone morto que não faz nada.
- Você não precisa reiniciar o telefone. Um aplicativo registrado enquanto alguém está com o telefone na mão aparece na tela na mesma hora.
O que um aplicativo pode dizer sobre si mesmo
Seção intitulada “O que um aplicativo pode dizer sobre si mesmo”id | letras, números, _ e -. Único em todo o servidor |
name | o nome exibido no ícone |
subtitle, description, features, version | a página da App Store |
developer, category | exibidos na App Store |
color | a cor de fundo do ícone quando não há icon |
icon | uma URL. https://cfx-nui-<resource>/path.png serve o arquivo a partir da sua própria pasta |
ui | resource/path/index.html, a página do seu aplicativo. Omita para um aplicativo que só executa Lua. Um caminho dentro de um recurso neste servidor, nunca uma URL externa — veja abaixo |
landscape | a página quer o telefone desdobrado |
size, images | o que a página da App Store mostra. As imagens devem usar https:// |
default | true coloca direto na Tela de Início em vez de na App Store |
onOpen, onClose | chamados quando o aplicativo abre e quando fecha |
Você não pode definir o próprio preço. Não existe um campo price, e isso é proposital: o número viveria no cliente, e um preço declarado pelo cliente é um preço que o cliente pode alterar — a App Store mostraria um valor e o servidor não cobraria nada. A precificação fica em Config.AppList.Custom, escrita pelo dono do servidor e lida pelo servidor. Se você quiser que o seu aplicativo custe dinheiro, peça para adicionarem uma linha lá; basta uma.
A cidade tem a última palavra. Tudo o que você pede aqui é uma solicitação. Config.AppList.Custom na configuração do próprio telefone pode colocar preço no seu aplicativo, entregá-lo pré-instalado ou desativá-lo por completo, sem que ninguém precise editar o seu recurso. Isso é deliberado — o dono de um servidor não deveria ter que modificar o código de um script para cobrar por ele no jogo.
Conversando com a sua página
Seção intitulada “Conversando com a sua página”A sua página é um iframe. Ela não consegue alcançar o interior do telefone, ler mensagens ou desenhar fora do próprio retângulo; o telefone também não consegue invadir o espaço dela. Tudo atravessa por meio de mensagens.
ui precisa ser um caminho dentro de um recurso neste servidor. Um frame precisa que a Content-Security-Policy (CSP) da página permita a sua origem, e a CSP não tem como dizer “qualquer recurso cfx-nui-” — um curinga só é permitido como um rótulo inteiro mais à esquerda, portanto https://cfx-nui-* não pode ser escrito e a diretiva precisa ser aberta para https: para que o seu frame possa carregar. Isso é amplo, então o telefone restringe do outro lado: ele constrói a origem a partir do próprio caminho do seu recurso e recusa qualquer coisa que já seja uma URL. Se quiser servir a sua página de outro lugar, sirva-a a partir da sua própria pasta.
Na sua página, duas linhas:
parent.postMessage({ type: 'phone:ready' }, '*'); // say you have loadedconst send = m => parent.postMessage({ type: 'phone:message', message: m }, '*');e escute:
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 */ }});No seu 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)Envie o que quiser a partir de onOpen. Uma mensagem para uma página que ainda não carregou fica aguardando e chega em ordem no exato momento em que a página envia phone:ready. Outros telefones avisam para você não fazer isso porque a mensagem deles aposta corrida com o carregamento do frame; aqui isso não acontece, porque enviar dados quando o aplicativo abre é a coisa óbvia a se fazer, e coisas óbvias devem funcionar.
phone:theme chega com o modo light/dark do telefone e o idioma assim que a sua página diz olá. Siga-o e o seu aplicativo deixará de parecer um site enxertado às pressas.
Pedindo algo ao telefone
Seção intitulada “Pedindo algo ao telefone”A sua página pode pedir ao telefone para fazer coisas que ele já sabe fazer — mostrar uma notificação, abrir a câmera, deixar alguém escolher uma foto ou um contato. Quinze linhas configuram tudo e, depois disso, cada pergunta é um simples 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| Pergunta | O que retorna | |
|---|---|---|
me | { number, name } | o que os outros telefones já veem, e mais nada sobre a pessoa |
notify | true | registrado sob o seu app, então silenciá-lo nos Ajustes silencia você |
toast | true | um aviso rápido na parte inferior da tela |
photos.pick | uma foto, ou null | abre a biblioteca do telefone; null significa que foi fechado sem escolher |
camera.take | uma foto, ou null | abre a Câmera real, aguarda e traz você de volta |
contacts.pick | um contato, ou null | abre uma lista de nomes |
theme | { theme, language, tablet } | os mesmos valores que phone:theme envia |
close | true | volta para a Tela de Início |
E a outra metade, que entrega algo de volta ao telefone:
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 }| Pergunta | O que retorna | |
|---|---|---|
message.compose | true | abre o app Mensagens com o texto preenchido. Um humano aperta enviar |
call.start | true | liga para o número |
share.text / share.photo | { to, number }, ou null | abre um seletor e prepara o envio para quem foi escolhido |
photos.save | { id, width, height } | salva uma imagem criada pelo seu app no app Fotos, como se fosse da câmera |
waypoint.set | true | marca um ponto no mapa |
money.balance | { balance } | apenas leitura — o dinheiro se move no servidor, veja abaixo |
Nada é enviado em nome de alguém sem que a pessoa veja. message.compose preenche o campo de texto e deixa o botão de enviar para o jogador; o compartilhamento pergunta para quem é todas as vezes. Um aplicativo que pudesse enviar mensagens silenciosamente para os seus contatos seria muito pior do que um aplicativo que não pode enviar mensagem alguma.
Dinheiro
Seção intitulada “Dinheiro”O dinheiro se move no servidor, a partir do seu próprio arquivo de servidor, porque um preço com o qual o cliente pode discutir não é um preço. Três portas de entrada, e qual usar depende de quem moveu o dinheiro:
-- 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)Todas as três atualizam a Carteira na tela imediatamente. Charge e Pay falham por completo ou têm sucesso por completo: uma cobrança recusada deixa o saldo e o histórico exatamente como estavam.
Dois princípios sustentam isso, e explicam por que a lista é enxuta em vez de permissiva:
O seu aplicativo nunca recebe dados que ninguém lhe entregou. contacts.pick não retorna a agenda inteira — ele abre uma lista, a pessoa toca em um nome e apenas esse nome retorna. Perguntar em loop não lê o telefone; cada resposta exige um toque do usuário. O mesmo vale para as fotos.
Apenas o aplicativo visível na tela pode perguntar. Uma pergunta vinda de um frame que não seja o aplicativo aberto no momento não é respondida e não abre nada, de modo que uma página deixada rodando em segundo plano não pode agir enquanto alguém está fazendo outra coisa.
Se o que você precisa não está nessa lista, não é porque o telefone se recusa — é apenas porque ninguém adicionou ainda. Adicionar um item é colocar uma entrada em PHONE_APP_ASKS dentro de html/apps/store/app-api.js e nada mais.
Perguntando sobre o telefone
Seção intitulada “Perguntando sobre o telefone”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 é o export que você deve usar quando o seu aplicativo não estiver aparecendo. O comando phoneapps do servidor imprime o que a configuração diz, que é uma lista diferente — o seu aplicativo vive no cliente que o registrou, e só aparece em phoneapps se alguém tiver criado uma entrada para ele em Config.AppList.Custom.
Um aplicativo que não seja default = true nem installed na configuração fica na App Store até que alguém o baixe. Se o seu está registrado e você não o encontra na Tela de Início, geralmente o motivo é esse.
Quando não funciona
Seção intitulada “Quando não funciona”| O que você vê | Quase sempre é |
|---|---|
| O aplicativo não aparece em lugar nenhum | Rode RegisteredApps() primeiro. Se não estiver lá, AddApp foi recusado — o motivo é impresso no console do cliente (F8), não no do servidor |
| Está registrado, mas não na Tela de Início | Está na App Store: você definiu default = false, ou o dono do servidor colocou How = 'store' em Config.AppList.Custom |
| O aplicativo abre uma página em branco | O seu arquivo ui não está no bloco files do seu fxmanifest.lua |
Refused to frame no console | O seu ui é uma URL. Ele precisa ser um caminho dentro de um recurso neste servidor |
| A página carrega, mas as mensagens nunca chegam | A sua página nunca enviou phone:ready. Nada é entregue até que ela envie — as mensagens ficam retidas, não perdidas, então chegam todas de uma vez |
no answer in 12000ms | Você chamou algo que o telefone não tem. Confira o nome na lista acima |
phoneapps no servidor imprime a configuração, não o seu aplicativo — um aplicativo registrado por um recurso vive no cliente que o registrou e só aparece lá se alguém tiver dado a ele uma entrada em Config.AppList.Custom. exports.mic_phone:RegisteredApps() é a lista que aquela máquina realmente possui.
O restante da API do telefone — state bags, chamadas, contatos, notificações — está em Exports e state bags.