diff --git a/js/app.js b/js/app.js
index d4e6c61..9f43bc0 100644
--- a/js/app.js
+++ b/js/app.js
@@ -14,6 +14,7 @@ const API = (location.pathname.indexOf('/password-manager/') === 0)
// Active only when running inside the Delphi-hosted WebView2 (API === '').
// Falls back to navigator.clipboard for the standalone PHP frontend.
const prefResolvers = {};
+let licenceResolver = null;
const fileSaveResolvers = {};
const fileChunkResolvers = {};
// Stream a base64 string to Delphi in URL-sized pieces via cmd://file/chunk,
@@ -293,6 +294,33 @@ const Bridge = (() => {
cmd('cmd://prefs/set?key=' + encodeURIComponent(key) +
'&value=' + encodeURIComponent(value || ''));
},
+ // Licence check / activation, verified in Delphi against the
+ // publisher's public key (PM.Licence). Hors du navigateur hôte il n'y
+ // a pas de licence à vérifier — le harnais de test et le dev en
+ // navigateur passent.
+ // ponytail: un seul résolveur, pas une table comme prefResolvers — les
+ // deux appels ne se croisent jamais (statut au démarrage, activation
+ // sur clic). Passer à une table le jour où ils se croiseraient.
+ licence(action, key) {
+ if (!active) return Promise.resolve({ ok: true, holder: '' });
+ return new Promise(resolve => {
+ licenceResolver = resolve;
+ cmd('cmd://licence/' + action +
+ (action === 'activate' ? '?key=' + encodeURIComponent(key || '') : ''));
+ setTimeout(() => {
+ if (licenceResolver === resolve) {
+ licenceResolver = null;
+ resolve({ ok: false, holder: '' });
+ }
+ }, 3000);
+ });
+ },
+ onLicenceResult(ok, holder) {
+ const r = licenceResolver;
+ if (!r) return;
+ licenceResolver = null;
+ r({ ok: !!ok, holder: holder || '' });
+ },
onPrefResult(key, value) {
const r = prefResolvers[key];
if (r) {
@@ -6908,6 +6936,43 @@ function resetAutoLock() {
}, { passive: true })
);
+// La porte d'entrée : sans licence valable, rien d'autre ne se peint.
+// Ne rend la main QUE sur une licence acceptée — il n'y a pas de bouton
+// "plus tard", c'est tout l'objet de l'écran.
+async function ensureLicence() {
+ if ((await Bridge.licence('status')).ok) return;
+ // Un démarrage à froid lent peut manquer la fenêtre de réponse, et le
+ // silence se lit alors comme un refus : redemander une fois coûte moins
+ // qu'un écran d'activation présenté à quelqu'un qui a déjà payé.
+ if ((await Bridge.licence('status')).ok) return;
+
+ const screen = $('#licenceScreen');
+ const hint = $('#licenceHint');
+ const input = $('#licenceKey');
+ screen.classList.remove('is-hidden');
+ input.focus();
+
+ await new Promise(resolve => {
+ $('#licenceForm').addEventListener('submit', async e => {
+ e.preventDefault();
+ const key = input.value.trim();
+ if (!key) return;
+ hint.textContent = 'Checking…';
+ const res = await Bridge.licence('activate', key);
+ if (!res.ok) {
+ // Une clé refusée ne dit pas POURQUOI : signature invalide,
+ // recopie fautive et charge utile trafiquée se ressemblent, et
+ // détailler n'aiderait que celui qui essaie d'en fabriquer une.
+ hint.textContent = 'This licence key is not valid.';
+ input.select();
+ return;
+ }
+ screen.classList.add('is-hidden');
+ resolve();
+ });
+ });
+}
+
async function showAuth() {
$('#authScreen').classList.remove('is-hidden');
$('#appShell').classList.add('is-hidden');
@@ -7353,6 +7418,10 @@ async function init() {
history.replaceState(null, '', location.pathname + location.hash);
}
+ // Avant tout le reste : un écran d'authentification qui s'affiche puis se
+ // fait recouvrir donnerait l'impression que le coffre était accessible.
+ await ensureLicence();
+
installCustomContextMenu();
document.addEventListener('keydown', e => {
const mod = e.ctrlKey || e.metaKey;