1/* 83-629.fr PWA bootstrap - 83629-pwa-v21-2026-07-22 */ 2(function(){ 3 'use strict'; 4 if (window.__m83629PwaBootstrapped) return; 5 window.__m83629PwaBootstrapped = true; 6 7 if ('serviceWorker' in navigator) { 8 window.addEventListener('load', function() { 9 navigator.serviceWorker.register('/sw.js', { scope: '/' }).then(function(reg) { 10 try { reg.update(); } catch (e) {} 11 }).catch(function() { /* silencieux */ }); 12 }); 13 } 14 15 // Audit PWA 2026-06 : tracker l'instant precis d'installation (listener inexistant 16 // jusqu'ici) + basculer install_status sur 'app' des l'install accepte. 17 window.addEventListener('appinstalled', function() { 18 gtag_safe('pwa_app_installed', { source: 'appinstalled' }); 19 try { if (window.gtag) window.gtag('set', 'user_properties', { install_status: 'app' }); } catch (e) {} 20 try { if (window.clarity) window.clarity('set', 'install_status', 'app'); } catch (e) {} 21 snooze(); 22 }); 23 24 // --- Web Push (Track C audit) : abonnement cible. Le cohorte installe est loyal 25 // (92% returning) et le produit est une veille JORF quotidienne -> levier de 26 // re-engagement #1. On ne PROPOSE l'opt-in qu'aux utilisateurs en mode app ; 27 // pour les autres deja autorises (granted), on resynchronise silencieusement. 28 function urlB64ToU8(s) { 29 s = (s + '='.repeat((4 - s.length % 4) % 4)).replace(/-/g, '+').replace(/_/g, '/'); 30 var raw = atob(s), out = new Uint8Array(raw.length); 31 for (var i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i); 32 return out; 33 } 34 function isStandaloneNow() { 35 return (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches) 36 || window.navigator.standalone === true 37 || (document.referrer || '').indexOf('android-app://') === 0; 38 } 39 function postSub(sub) { 40 try { 41 fetch('/api/push/subscribe', { 42 method: 'POST', headers: { 'Content-Type': 'application/json' }, keepalive: true, 43 body: JSON.stringify({ subscription: sub.toJSON ? sub.toJSON() : sub, install_status: isStandaloneNow() ? 'app' : 'browser' }) 44 }).catch(function(){}); 45 } catch (e) {} 46 } 47 function subscribePush(reg) { 48 try { 49 reg.pushManager.getSubscription().then(function(sub) { 50 if (sub) { postSub(sub); return; } 51 reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlB64ToU8(window.__b83Vapid) }) 52 .then(postSub).catch(function(){}); 53 }).catch(function(){}); 54 } catch (e) {} 55 } 56 function pushOptIn() { 57 if (!('serviceWorker' in navigator) || !('PushManager' in window) || !window.__b83Vapid) return; 58 if (typeof Notification === 'undefined') return; 59 navigator.serviceWorker.ready.then(function(reg) { 60 if (Notification.permission === 'granted') { subscribePush(reg); return; } 61 if (Notification.permission === 'denied') return; 62 if (!isStandaloneNow()) return; 63 var PKEY = 'm83629-push-snooze'; 64 try { var ps = parseInt(localStorage.getItem(PKEY) || '0', 10); if (ps && Date.now() < ps) return; } catch (e) {} 65 var bar = document.createElement('div'); 66 bar.setAttribute('role', 'dialog'); 67 bar.style.cssText = 'position:fixed;left:12px;right:12px;bottom:12px;max-width:520px;margin:0 auto;background:#1e3a8a;color:#fff;border-radius:12px;padding:12px 14px;box-shadow:0 12px 32px rgba(15,30,80,.28);font:14px -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;z-index:99991;display:flex;align-items:center;gap:10px'; 68 bar.innerHTML = '<span style="flex:1;line-height:1.35">\ud83d\udd14 Activer les alertes veille JORF\u202f? Un signal d\u00e8s qu\u2019un texte du recueil change.</span>' 69 + '<button type="button" class="ok" style="background:#fff;color:#1e3a8a;border:none;border-radius:8px;padding:9px 14px;font-weight:700;cursor:pointer">Activer</button>' 70 + '<button type="button" class="no" aria-label="Plus tard" style="background:none;border:none;color:#cdd7f5;font-size:22px;cursor:pointer;padding:0 4px">\u00d7</button>'; 71 document.body.appendChild(bar); 72 function close(days) { bar.remove();
72 try { localStorage.setItem(PKEY, String(Date.now() + days * 86400000)); } catch (e) {} } 73 bar.querySelector('.no').addEventListener('click', function() { close(30); }); 74 bar.querySelector('.ok').addEventListener('click', function() { 75 Notification.requestPermission().then(function(p) { 76 if (p === 'granted') { subscribePush(reg); gtag_safe('push_opt_in', { result: 'granted' }); } 77 else gtag_safe('push_opt_in', { result: p }); 78 close(90); 79 }); 80 }); 81 gtag_safe('push_optin_shown', {}); 82 }).catch(function(){}); 83 } 84 pushOptIn(); 85 86 var isStandalone = window.matchMedia('(display-mode: standalone)').matches 87 || window.navigator.standalone === true 88 || (document.referrer || '').indexOf('android-app://') === 0; 89 if (isStandalone) return; 90 91 // Audit PWA 2026-06 : ne pas montrer la banniere dans les navigateurs in-app (Facebook, 92 // Instagram, LinkedIn, TikTok...) ou l'installation / A2HS est impossible -> frustration. 93 if (/FBAN|FBAV|FB_IAB|Instagram|LinkedInApp|Twitter|Snapchat|Pinterest|musical_ly|TikTok|Line\/|GSA\//i.test(navigator.userAgent)) return; 94 95 // Snooze robuste : stocke une date d'EXPIRATION (localStorage + cookie de secours, car 96 // localStorage throw en navigation privee / contexte embarque -> 24 erreurs Clarity). 97 var SNOOZE_KEY = 'm83629-pwa-snoozed'; 98 function snoozeUntil() { 99 var v = 0; 100 try { v = parseInt(localStorage.getItem(SNOOZE_KEY) || '0', 10) || 0; } catch (e) {} 101 if (!v) { var m = (document.cookie.match(/(?:^|;\s*)m83629ps=(\d+)/) || [])[1]; v = m ? parseInt(m, 10) : 0; } 102 return v; 103 } 104 if (Date.now() < snoozeUntil()) return; 105 106 // Audit PWA 2026-06 : ne pas solliciter des la 1re visite (fatigue : 2,5 impressions / 107 // utilisateur desktop). Montrer si visiteur connu OU >= 2 pages vues dans la session. 108 var __engaged = true; 109 try { 110 var __pv = (parseInt(sessionStorage.getItem('m83629-pv') || '0', 10) || 0) + 1; 111 sessionStorage.setItem('m83629-pv', String(__pv)); 112 var __known = !!localStorage.getItem('m83629-seen'); 113 localStorage.setItem('m83629-seen', '1'); 114 __engaged = __known || __pv >= 2; 115 } catch (e) { __engaged = true; } 116 if (!__engaged) return; 117 118 var style = document.createElement('style'); 119 style.textContent = [ 120 '.m83629-pwa-ib{position:fixed;left:16px;right:16px;bottom:16px;max-width:520px;margin:0 auto;background:#fff;color:#0E1A2B;border:1px solid #e5e7eb;border-left:4px solid #0A1E3D;box-shadow:0 12px 32px rgba(15,30,80,.18);border-radius:12px;padding:14px 16px;font:14px -apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;z-index:99990;display:none}', 121 '.m83629-pwa-ib.show{display:flex;align-items:flex-start;gap:12px;animation:m83629PwaIn .25s ease-out}', 122 '@keyframes m83629PwaIn{from{transform:translateY(20px);opacity:0}to{transform:none;opacity:1}}', 123 '.m83629-pwa-ib .ico{flex:0 0 40px;width:40px;height:40px;border-radius:10px;background:#0A1E3D;display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:14px;letter-spacing:1px}', 124 '.m83629-pwa-ib .txt{flex:1;line-height:1.4;min-width:0}', 125 '.m83629-pwa-ib .h{margin:0 0 2px;font-weight:700;font-size:14.5px;color:#0E1A2B}', 126 '.m83629-pwa-ib .d{margin:0;color:#475569;font-size:13px}', 127 '.m83629-pwa-ib .act{display:flex;gap:8px;flex-shrink:0;align-self:center}', 128 '.m83629-pwa-ib .b{background:#0A1E3D;color:#fff;border:none;border-radius:8px;padding:9px 14px;font-weight:700;font-size:13px;cursor:pointer}', 129 '.m83629-pwa-ib .b:hover{background:#14245a}', 130 '.m83629-pwa-ib .x{background:none;border:none;color:#94a3b8;font-size:22px;cursor:pointer;padding:0 4px;line-height:1}', 131 '.m83629-pwa-ib .x:hover{color:#475569}', 132 ':root[data-theme="dark"] .m83629-pwa-ib{background:#1e1e2e;color:#e8ecf6!important;border-color:#374151;border-left-color:#60a5fa}', 133 ':root[data-theme="dark"] .m83629-pwa-ib .h{color:#f1f5f9!important}', 134 ':root[data-theme="dark"] .m83629-pwa-ib .d{color:#94a3b8!important}', 135 ':root[data-theme="dark"] .m83629-pwa-ib .x{color:#94a3b8!important}', 136 ':root[data-theme="dark"] .m83629-pwa-ib .ico{background:#60a5fa;color:#0e1017!important}', 137 ':root[data-theme="dark"] .m83629-pwa-ib .b{background:#60a5fa;color:#0e1017!important}', 138 '@media(max-width:520px){.m83629-pwa-ib{flex-wrap:wrap}.m83629-pwa-ib .act{width:100%;justify-content:flex-end;margin-top:4px}}' 139 ].join('\n'); 140 document.head.appendChild(style); 141 142 function snoozeFor(days) { 143 var until = Date.now() + days * 86400000;
144 try { localStorage.setItem(SNOOZE_KEY, String(until)); } catch (e) {} 145 try { document.cookie = 'm83629ps=' + until + '; Path=/; Max-Age=' + (days * 86400) + '; SameSite=Lax'; } catch (e) {} 146 } 147 // decision prise (fermeture / installation) : ne plus solliciter pendant 90 jours. 148 function snooze() { snoozeFor(90); } 149 function gtag_safe(name, params) { 150 if (typeof window.gtag_safe === 'function') window.gtag_safe(name, params); 151 } 152 // Audit PWA 2026-06 : ne pas concurrencer le bandeau de consentement Iubenda en bas 153 // d'écran (mobile). Tant qu'il est visible, on diffère la bannière d'install. 154 function consentPending() { 155 try { 156 var b = document.getElementById('iubenda-cs-banner'); 157 return !!(b && b.offsetParent !== null); 158 } catch (e) { return false; } 159 } 160 161 function buildBanner(opts) { 162 var w = document.createElement('div'); 163 w.className = 'm83629-pwa-ib'; 164 w.setAttribute('role', 'dialog'); 165 w.setAttribute('aria-label', 'Installer 83-629.fr'); 166 w.innerHTML = '' 167 + '<div class="ico" aria-hidden="true">83</div>' 168 + '<div class="txt">' 169 + '<p class="h">' + opts.title + '</p>' 170 + '<p class="d">' + opts.desc + '</p>' 171 + '</div>' 172 + '<div class="act">' 173 + (opts.actionLabel ? '<button class="b" type="button">' + opts.actionLabel + '</button>' : '') 174 + '<button class="x" type="button" aria-label="Fermer">\u00d7</button>' 175 + '</div>'; 176 document.body.appendChild(w); 177 requestAnimationFrame(function() { w.classList.add('show'); }); 178 var bActive = w.querySelector('.b'); 179 var bClose = w.querySelector('.x'); 180 bClose.addEventListener('click', function() { 181 w.remove(); snooze(); 182 gtag_safe('pwa_install_banner_dismissed', { source: opts.source || 'beforeinstallprompt' }); 183 }); 184 if (bActive && opts.onAction) bActive.addEventListener('click', function() { opts.onAction(w); }); 185 gtag_safe('pwa_install_banner_seen', { source: opts.source || 'beforeinstallprompt' }); 186 // Anti-fatigue : une fois vue, ne pas re-solliciter avant 14 j (snooze plein de 90 j 187 // si l'utilisateur ferme ou installe). Coupe les 2,5 impressions/utilisateur desktop. 188 snoozeFor(14); 189 return w; 190 } 191 192 var deferredPrompt = null; 193 window.addEventListener('beforeinstallprompt', function(e) { 194 e.preventDefault(); 195 deferredPrompt = e; 196 var tries = 0; 197 function showWhenReady() { 198 if (consentPending() && tries++ < 12) { setTimeout(showWhenReady, 2000); return; } 199 buildBanner({ 200 title: 'Installer l\u2019app 83-629.fr', 201 desc: 'Acc\u00e8s rapide au recueil, \u00e0 la veille et aux tests de veille, m\u00eame sans r\u00e9seau.', 202 actionLabel: 'Installer', 203 source: 'chromium', 204 onAction: function(w) { 205 if (!deferredPrompt) return; 206 deferredPrompt.prompt(); 207 deferredPrompt.userChoice.then(function(choice) { 208 gtag_safe('pwa_install_prompt_result', { outcome: choice && choice.outcome }); 209 deferredPrompt = null; 210 w.remove(); 211 snooze(); 212 }); 213 } 214 }); 215 } 216 setTimeout(showWhenReady, 3000); 217 }); 218 219 var ua = navigator.userAgent; 220 var isiOS = /iPad|iPhone|iPod/.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); 221 var isSafari = /Safari/.test(ua) && !/CriOS|FxiOS|Chrome|EdgiOS/.test(ua); 222 if (isiOS && isSafari) { 223 var iosTries = 0; 224 function showIosWhenReady() { 225 if (consentPending() && iosTries++ < 12) { setTimeout(showIosWhenReady, 2000); return; } 226 if (document.querySelector('.m83629-pwa-ib')) return; 227 buildBanner({ 228 title: 'Ajouter 83-629.fr \u00e0 l\u2019\u00e9cran d\u2019accueil', 229 desc: 'Bouton Partager \u2192 \u00ab Sur l\u2019\u00e9cran d\u2019accueil \u00bb.', 230 source: 'ios-safari', 231 actionLabel: null, 232 onAction: null 233 }); 234 } 235 setTimeout(showIosWhenReady, 5000); 236 } 237})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.