/*!
* plicflix.js 2.4 — o SDK do jogo parceiro. Sem dependências.
* Guia completo: https://developers.plicflix.com/docs/SDK
* Para IAs: https://developers.plicflix.com/llms.txt
*
*
*
*
* FORA do app (o seu site, o seu app): "Entrar com Plicflix" — OAuth 2.0 + PKCE (guia: /docs/OAUTH)
* Plicflix.botaoEntrar("#entrar", { cliente: "seu-jogo", volta: "https://seu-jogo.com/volta" });
* // na página de volta:
* const r = await Plicflix.voltouDoPlicflix({ servidor: "/api/plicflix/oauth" }); // ou sem servidor
* // r.jogador = { id: "pf_…", nome } — o MESMO id da entrada pelo app
*
* Regras (o guia explica cada uma):
* - o plicflix nunca hospeda o seu jogo: abre o SEU servidor por cima do app;
* - o jogo recebe APENAS UM TOKEN (5 minutos ou uma troca); quem descobre o
* jogador é o seu servidor, trocando o token com a sua chave pk_…;
* - do jogador chegam só o código ("pf_…", fixo no seu jogo) e o apelido;
* - a bolha do plicflix fica por cima do jogo (avisos, amigos, sair): o jogo
* conversa com ela por este SDK.
*/
(function (global) {
"use strict";
var VERSAO = "2.4.0";
var CHAVE_OAUTH = "plicflix:oauth";
var APP = "https://plicflix.com";
var CHAVE = "plicflix:ingresso";
var CHAVE_JOGADOR = "plicflix:jogador";
var token = null;
var api = "https://api.plicflix.com";
var ctx = { ref: "plicflix", idioma: "pt-BR", plataforma: "web" };
// ---------------------------------------------------------------- o endereço
// O app abre: https://seu-jogo/?ref=plicflix#plicflix=&pf_idioma=pt-BR&pf_plataforma=web
try {
var resto = [];
global.location.hash.replace(/^#/, "").split("&").forEach(function (par) {
var i = par.indexOf("="),
k = i < 0 ? par : par.slice(0, i),
v = i < 0 ? "" : decodeURIComponent(par.slice(i + 1));
if (k === "plicflix") token = v;
else if (k === "pf_idioma") ctx.idioma = v;
else if (k === "pf_plataforma") ctx.plataforma = v;
else if (k === "pf_api") api = v;
else if (par) resto.push(par);
});
if (token) {
// tira o token da barra de endereço (não vaza se alguém copiar o link)
global.history.replaceState(global.history.state, "", global.location.pathname + global.location.search + (resto.length ? "#" + resto.join("&") : ""));
global.sessionStorage.setItem(CHAVE, JSON.stringify({ token: token, ctx: ctx, api: api }));
} else {
// recarregou antes de trocar o token: ele continua nesta aba
var guardado = JSON.parse(global.sessionStorage.getItem(CHAVE) || "null");
if (guardado) {
token = guardado.token;
ctx = guardado.ctx;
if (guardado.api) api = guardado.api;
}
}
} catch (e) {
/* sem sessionStorage: segue só com o fragmento */
}
var dentro = !!token || /[?&]ref=plicflix(&|$)/.test(global.location.search);
// ---------------------------------------------------------------- jogo → app
function avisarApp(msg) {
msg.v = 2;
// celular (WebView do app)
if (global.PlicflixApp && global.PlicflixApp.postMessage) global.PlicflixApp.postMessage(JSON.stringify(msg));
// web (iframe dentro do app)
if (global.parent && global.parent !== global) global.parent.postMessage({ plicflix: msg }, "*");
}
// ---------------------------------------------------------------- app → jogo
var aoSairFn = null,
aoIrParaFn = null,
saindo = false;
function comoResultado(r) {
if (r === undefined || r === null || r === true) return { ok: true };
if (r === false) return { ok: false };
return { ok: r.ok !== false, motivo: r.motivo ? String(r.motivo).slice(0, 160) : undefined };
}
function receber(msg) {
if (!msg) return;
if (msg.tipo === "saindo") {
if (saindo) return;
saindo = true;
var fim = function () {
avisarApp({ tipo: "saiu" });
};
try {
Promise.resolve(aoSairFn ? aoSairFn() : null).then(fim, fim);
} catch (e) {
fim();
}
} else if (msg.tipo === "irPara") {
var responder = function (r) {
var res = comoResultado(r);
avisarApp({ tipo: "foiPara", ok: res.ok, motivo: res.motivo, pedido: msg.pedido });
};
if (!aoIrParaFn) return responder({ ok: false, motivo: "Este jogo ainda não sabe levar você até outra pessoa." });
try {
Promise.resolve(aoIrParaFn(msg.destino || {})).then(responder, function (e) {
responder({ ok: false, motivo: (e && e.message) || "Não foi possível ir até lá." });
});
} catch (e) {
responder({ ok: false, motivo: e.message });
}
}
}
// web: o app (a página que contém o iframe) manda por postMessage
global.addEventListener("message", function (e) {
if (e.source === global.parent && e.data && e.data.plicflixApp) receber(e.data.plicflixApp);
});
// celular: o app chama esta função pela WebView
global.__plicflixApp = function (msg) {
receber(typeof msg === "string" ? JSON.parse(msg) : msg);
};
var ultimoOnde = "";
var Plicflix = {
versao: VERSAO,
// ---- entrada ----
/** true quando o jogo foi aberto pelo plicflix. */
dentro: function () {
return dentro;
},
/** O token de uso único (ou null). O SEU servidor troca na API do plicflix. */
token: function () {
return token;
},
/** { ref: "plicflix", idioma: "pt-BR", plataforma: "web" | "android" | "ios" } */
contexto: function () {
return { ref: ctx.ref, idioma: ctx.idioma, plataforma: ctx.plataforma };
},
/**
* Manda o token para o SEU servidor (POST JSON { token, contexto }) e
* devolve a resposta dele. Depois o token é esquecido (vale uma vez só).
*/
entrar: function (url, opcoes) {
if (!token) return Promise.reject(new Error("plicflix: sem token (o jogo não foi aberto pelo plicflix)"));
opcoes = opcoes || {};
return global
.fetch(url, {
method: "POST",
credentials: opcoes.credentials || "include",
headers: Object.assign({ "Content-Type": "application/json" }, opcoes.headers || {}),
body: JSON.stringify({ token: token, contexto: Plicflix.contexto() }),
})
.then(function (r) {
try {
global.sessionStorage.removeItem(CHAVE);
} catch (e) {}
token = null;
return r.json().then(function (j) {
if (!r.ok) throw Object.assign(new Error((j && (j.erro || j.error)) || "plicflix: o servidor do jogo recusou o login"), { status: r.status, resposta: j });
return j;
});
});
},
/**
* Jogo SEM servidor: troca o token direto da página e devolve
* { jogador: { id, nome }, jogo, contexto, pelo_navegador: true }.
* Só funciona na origem cadastrada do jogo. Como tudo que roda no navegador,
* o resultado pode ser forjado por quem quiser: serve para mostrar o nome e
* guardar progresso local, nunca para proteger moedas, ranking ou compras
* (para isso, troque o token no SEU servidor com Plicflix.entrar).
* Recarregou a página? Devolve o mesmo jogador (fica guardado nesta aba).
*/
jogador: function () {
try {
var ja = JSON.parse(global.sessionStorage.getItem(CHAVE_JOGADOR) || "null");
if (ja) return Promise.resolve(ja);
} catch (e) {}
if (!token) return Promise.reject(new Error("plicflix: sem token (o jogo não foi aberto pelo plicflix)"));
return global
.fetch(api.replace(/\/$/, "") + "/v1/ingresso/navegador", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: token }),
})
.then(function (r) {
return r.json().then(function (j) {
if (!r.ok) throw Object.assign(new Error((j && j.erro) || "plicflix: troca recusada"), { status: r.status, resposta: j });
try {
global.sessionStorage.removeItem(CHAVE);
global.sessionStorage.setItem(CHAVE_JOGADOR, JSON.stringify(j));
} catch (e) {}
token = null;
return j;
});
});
},
// ---- carregamento ----
/** Andamento do carregamento (0–100) e um texto curto: a tela do plicflix mostra. */
progresso: function (pct, texto) {
avisarApp({ tipo: "progresso", pct: Math.max(0, Math.min(100, +pct || 0)), texto: texto ? String(texto).slice(0, 80) : "" });
},
/** O jogador já pode jogar: o plicflix tira a tela de carregamento. */
pronto: function () {
avisarApp({ tipo: "pronto" });
},
/** Deu errado (servidor fora, manutenção…): o plicflix mostra a mensagem e oferece tentar de novo. */
erro: function (texto) {
avisarApp({ tipo: "erro", texto: String(texto || "").slice(0, 160) });
},
// ---- sair ----
/**
* O jogador vai sair (a bolha, o voltar do celular…): fn roda antes de o
* app fechar o jogo — salve o progresso ali. Pode devolver uma Promise; o
* app espera no máximo 5 s.
*/
aoSair: function (fn) {
aoSairFn = typeof fn === "function" ? fn : null;
},
/** O próprio jogo fecha e volta ao catálogo (depois de já ter salvo). */
sair: function () {
avisarApp({ tipo: "sair" });
},
// ---- onde o jogador está, amigos ----
/**
* Onde o jogador está. Chame sempre que ele mudar de lugar.
* Plicflix.onde("Taverna do Porto")
* Plicflix.onde({ texto: "Taverna do Porto", // o que as pessoas leem (até 60 letras)
* area: "cidade/taverna", // para agrupar (minúsculas, números, / _ . -)
* ponto: { cena: "taverna:1", x: 12, y: 8 } }) // só o SEU jogo entende (até 1 KB)
* O plicflix mostra o texto na bolha e aos amigos (se a privacidade deixar),
* guarda o "continuar de onde parou" e devolve o ponto ao seu jogo no "ir até".
*/
onde: function (o) {
if (typeof o === "string") o = { texto: o };
if (!o || !o.texto) return;
var msg = { tipo: "onde", texto: String(o.texto).slice(0, 60) };
if (o.area) msg.area = String(o.area).toLowerCase().slice(0, 60);
if (o.ponto !== undefined && o.ponto !== null) {
var json = JSON.stringify(o.ponto);
if (typeof o.ponto !== "object" || json.length > 1024) {
if (global.console) console.warn("plicflix: onde.ponto precisa ser um objeto de até 1 KB — ignorado");
} else msg.ponto = o.ponto;
}
var chave = JSON.stringify(msg);
if (chave === ultimoOnde) return; // o mesmo lugar: não repete
ultimoOnde = chave;
avisarApp(msg);
},
/**
* "Ir até o amigo": o jogador escolheu ir até alguém que está no SEU jogo.
* fn recebe o destino e deve levar o jogador até lá:
* { jogador_id: "pf_…", // o código de quem está lá, no SEU jogo — use no SEU servidor
* jogador: "Ana", // o apelido
* texto: "Taverna do Porto", area: "cidade/taverna",
* ponto: { … } } // o que você mesmo mandou em Plicflix.onde (pode faltar)
* Devolva true / false, ou { ok, motivo } (pode ser Promise). O motivo
* aparece para o jogador ("Ela está numa terra particular").
* IMPORTANTE: não teletransporte pelo ponto vindo do navegador — mande o
* jogador_id para o seu servidor e ache lá a posição real.
*/
aoIrPara: function (fn) {
aoIrParaFn = typeof fn === "function" ? fn : null;
},
/** Abre a lista de amigos da bolha (ex.: um botão "Chamar amigos" no seu jogo). */
abrirAmigos: function () {
avisarApp({ tipo: "abrirAmigos" });
},
// ---- "Entrar com Plicflix" (fora do app: OAuth 2.0 + PKCE — /docs/OAUTH) ----
/**
* Manda o navegador para o plicflix (o jogador entra e autoriza) e volta para
* `volta` com ?code=…&state=…. O PKCE e o state ficam nesta aba.
* opcoes: { cliente: "seu-slug", volta: "https://…" (cadastrado no developers),
* prompt?: "consent" (pergunta mesmo se já autorizou), plicflix?: "https://plicflix.com" }
*/
entrarComPlicflix: function (opcoes) {
opcoes = opcoes || {};
if (!opcoes.cliente || !opcoes.volta) return Promise.reject(new Error("plicflix: informe { cliente, volta }"));
var verificador = aleatorioUrl(48),
estado = aleatorioUrl(16);
return desafioS256(verificador).then(function (desafio) {
try {
global.sessionStorage.setItem(CHAVE_OAUTH, JSON.stringify({ verificador: verificador, estado: estado, cliente: opcoes.cliente, volta: opcoes.volta, api: opcoes.api || api }));
} catch (e) {}
var q = "response_type=code&client_id=" + encodeURIComponent(opcoes.cliente) + "&redirect_uri=" + encodeURIComponent(opcoes.volta) +
"&state=" + estado + "&code_challenge=" + desafio + "&code_challenge_method=S256&scope=perfil" + (opcoes.prompt ? "&prompt=" + encodeURIComponent(opcoes.prompt) : "");
global.location.assign((opcoes.plicflix || APP).replace(/\/$/, "") + "/oauth/authorize?" + q);
});
},
/**
* Na página de volta: lê ?code / ?error, confere o state e troca o código.
* - opcoes.servidor: a rota do SEU servidor — recebe POST JSON { code, code_verifier, redirect_uri },
* troca com a chave pk_ (sdk/servidor) e já faz o login; a resposta dele volta aqui. (Recomendado.)
* - sem servidor: troca direto na API (cliente público) e devolve { jogador: { id, nome }, jogo,
* assinante, access_token }. Como tudo no navegador: não protege nada de valor.
* Sem ?code na URL: null. O jogador recusou: erro com .codigo = "access_denied".
*/
voltouDoPlicflix: function (opcoes) {
opcoes = opcoes || {};
var q = new URLSearchParams(global.location.search),
codigo = q.get("code"),
estado = q.get("state"),
falha = q.get("error");
if (!codigo && !falha) return Promise.resolve(null);
var guardado = null;
try {
guardado = JSON.parse(global.sessionStorage.getItem(CHAVE_OAUTH) || "null");
global.sessionStorage.removeItem(CHAVE_OAUTH);
} catch (e) {}
["code", "state", "error", "error_description"].forEach(function (k) { q.delete(k); });
var resto = q.toString();
global.history.replaceState(global.history.state, "", global.location.pathname + (resto ? "?" + resto : "") + global.location.hash);
if (falha) return Promise.reject(Object.assign(new Error("plicflix: " + falha), { codigo: falha }));
if (!guardado || guardado.estado !== estado) return Promise.reject(Object.assign(new Error("plicflix: state não confere (abra o login de novo)"), { codigo: "state" }));
var corpo = { code: codigo, code_verifier: guardado.verificador, redirect_uri: guardado.volta };
if (opcoes.servidor) {
return global
.fetch(opcoes.servidor, { method: "POST", credentials: opcoes.credentials || "include", headers: Object.assign({ "Content-Type": "application/json" }, opcoes.headers || {}), body: JSON.stringify(corpo) })
.then(lerResposta);
}
corpo.grant_type = "authorization_code";
corpo.client_id = guardado.cliente;
return global
.fetch((opcoes.api || guardado.api || api).replace(/\/$/, "") + "/v1/oauth/token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(corpo) })
.then(lerResposta);
},
/**
* O botão oficial "Entrar com Plicflix" (ou "Sign in with Plicflix"…, pela língua da página).
* Plicflix.botaoEntrar("#onde", { cliente, volta, texto?, largura?: "100%", tema?: "escuro" | "claro" })
* Ou um link para a SUA rota que começa o login no servidor: { href: "/api/plicflix/oauth" }.
* Devolve o elemento.
*/
botaoEntrar: function (onde, opcoes) {
opcoes = opcoes || {};
var el = typeof onde === "string" ? global.document.querySelector(onde) : onde;
if (!el) throw new Error("plicflix: botaoEntrar — elemento não encontrado");
var b = global.document.createElement(opcoes.href ? "a" : "button");
if (opcoes.href) b.href = opcoes.href;
else b.type = "button";
b.className = "plicflix-entrar";
var claro = opcoes.tema === "claro";
b.setAttribute("style", "display:inline-flex;align-items:center;justify-content:center;gap:10px;box-sizing:border-box;min-height:44px;padding:10px 18px;border-radius:12px;" +
"font:700 15px/1.2 system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;text-decoration:none;cursor:pointer;border:" + (claro ? "1px solid #d9d2ef" : "0") + ";" +
"color:" + (claro ? "#1a1033" : "#fff") + ";background:" + (claro ? "#fff" : "linear-gradient(90deg,#DC0DF7,#A104F8)") + ";width:" + (opcoes.largura || "auto") + ";");
b.innerHTML = '';
var t = global.document.createElement("span");
t.textContent = opcoes.texto || textoDoBotao();
b.appendChild(t);
if (!opcoes.href) b.addEventListener("click", function () {
b.disabled = true;
Plicflix.entrarComPlicflix(opcoes).catch(function (e) {
b.disabled = false;
if (global.console) console.error(e);
});
});
el.appendChild(b);
return b;
},
};
// ---------------------------------------------------------------- ajudantes do OAuth
function aleatorioUrl(n) {
var b = new Uint8Array(n);
global.crypto.getRandomValues(b);
return base64url(b);
}
function base64url(bytes) {
var s = "";
for (var i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
return global.btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function desafioS256(v) {
return global.crypto.subtle.digest("SHA-256", new TextEncoder().encode(v)).then(function (h) {
return base64url(new Uint8Array(h));
});
}
function lerResposta(r) {
return r.json().catch(function () { return {}; }).then(function (j) {
if (!r.ok) throw Object.assign(new Error("plicflix: " + ((j && (j.error_description || j.erro || j.error)) || r.status)), { status: r.status, codigo: j && j.error, resposta: j });
return j;
});
}
function textoDoBotao() {
var l = ((global.document && global.document.documentElement.lang) || (global.navigator && global.navigator.language) || "en").toLowerCase();
if (l.indexOf("pt") === 0) return "Entrar com Plicflix";
if (l.indexOf("es") === 0) return "Entrar con Plicflix";
if (l.indexOf("fr") === 0) return "Se connecter avec Plicflix";
if (l.indexOf("de") === 0) return "Mit Plicflix anmelden";
if (l.indexOf("it") === 0) return "Accedi con Plicflix";
return "Sign in with Plicflix";
}
global.Plicflix = Plicflix;
if (typeof module === "object" && module.exports) module.exports = Plicflix;
if (dentro) avisarApp({ tipo: "ola", versao: VERSAO, recursos: ["entrar", "jogador", "carregamento", "sair", "onde", "irPara", "amigos"] });
})(typeof window !== "undefined" ? window : this);