1/** 2 * PWA ã¤ã³ã¹ãã¼ã«æ¤ç¥ + GA4 èµ·åã¤ãã³ãçºç« + DB ä¿å 3 * 4 * spec: docs/features/pwa-installation/spec.md §2.3 / §2.8 / §5.10 5 * 6 * - standalone ã¢ã¼ãã㤠sessionStorage ã空ï¼= èµ·åæï¼ã« GA4 ã¤ãã³ãçºç« 7 * - ãã°ã¤ã³æ¸ã¿ãªã DB POSTãæªãã°ã¤ã³ãªã次åãã°ã¤ã³å¾ã«ç¶æ å¤åæ¤ç¥ã§ POST 8 * - localStorage ã¯ä½¿ç¨ããªãï¼sessionStorage ã®ã¿ãiOS ITP å½±é¿ãåããªãï¼ 9 */ 10(function () { 11 var SESSION_KEY = 'pwa_session_state'; 12 13 function isStandalone() { 14 return (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches) 15 || window.navigator.standalone === true; 16 } 17 18 function detectDeviceType() { 19 var ua = window.navigator.userAgent || ''; 20 if (/iPhone|iPad|iPod/i.test(ua)) return 'ios'; 21 if (/Android/i.test(ua)) return 'android'; 22 return 'other'; 23 } 24 25 function getPrevState() { 26 try { 27 return JSON.parse(sessionStorage.getItem(SESSION_KEY) || '{}'); 28 } catch (e) { 29 return {}; 30 } 31 } 32 33 function setState(state) { 34 try { 35 sessionStorage.setItem(SESSION_KEY, JSON.stringify(state)); 36 } catch (e) { 37 // sessionStorage å©ç¨ä¸å¯ï¼ãã©ã¤ãã¼ããã©ã¦ã¸ã³ã°çï¼ã®å ´åã¯é»ã£ã¦å¤±æ 38 } 39 } 40 41 // GA4 ã¤ãã³ãçºç«ã®å ±é颿°ãGA4Events.php ã®å®æ°ã¨ä¸è´ããã 42 function sendGa4Event(eventName, params) { 43 params = params || {}; 44 if (typeof window.gtag === 'function') { 45 window.gtag('event', eventName, params); 46 } else if (window.dataLayer && Array.isArray(window.dataLayer)) { 47 var payload = { event: eventName }; 48 for (var k in params) { 49 if (Object.prototype.hasOwnProperty.call(params, k)) payload[k] = params[k]; 50 } 51 window.dataLayer.push(payload); 52 } 53 } 54 55 function sendGa4LaunchEvent() { 56 // standalone èµ·åæã¤ãã³ããdevice 㯠GA4 èªååéã® operating_system ã§éè¨å¯è½ 57 sendGa4Event('pwa_launch_standalone'); 58 } 59 60 function sendInstallationPost(url, deviceType, token) { 61 return fetch(url, { 62 method: 'POST', 63 credentials: 'same-origin', 64 headers: { 65 'Accept': 'application/json', 66 'Content-Type': 'application/json', 67 'X-CSRF-TOKEN': token, 68 }, 69 body: JSON.stringify({ device_type: deviceType }), 70 }); 71 } 72 73 // 419 ãªãã©ã¤ç¨: ç¾å¨ã»ãã·ã§ã³ã® fresh CSRF token ãåå¾ã 74 // meta ã¿ã° token ã staleï¼session åçæå¾ã»iOS PWA cookie åé¢ã³ã³ããã¹ãçï¼ã§ã復æ§ã§ããã 75 // 失æç³»ï¼404 / 5xx / JSON parse 失æ / token ãæååã§ãªãã»ç©ºï¼ã¯ null ãè¿ãã¦ãªãã©ã¤ä¸æ¢ã 76 function fetchFreshCsrfToken() { 77 var url = window.CSRF_TOKEN_URL; 78 if (!url) return Promise.resolve(null); 79 return fetch(url, { credentials: 'same-origin', headers: { 'Accept': 'application/json' } }) 80 .then(function (res) { return res.ok ? res.json() : null; }) 81 .then(function (data) { 82 if (!data || typeof data.token !== 'string' || data.token === '') return null; 83 return data.token; 84 }) 85 .catch(function () { return null; }); 86 } 87 88 function postInstallation(deviceType) { 89 var url = window.PWA_INSTALLATIONS_URL; 90 if (!url) return; 91 var csrfTokenMeta = document.querySelector('meta[name="csrf-token"]'); 92 var csrfToken = csrfTokenMeta ? csrfTokenMeta.getAttribute('content') : ''; 93 94 sendInstallationPost(url, deviceType, csrfToken).then(function (res) { 95 // 419 ã®å ´åã®ã¿ fresh token ãåãç´ã㦠1 度ã ããªãã©ã¤ããã 96 // res.status===419 ã¯ãmeta ã¿ã°ã® token ã staleãã®ã·ã°ãã«ã 97 // 200 / 401 / 500 çã®é 419 ã¯ãªãã©ã¤ãã¦ãçµæãå¤ãããªãããæ¢åæåã©ããçµäºã 98 if (res.status !== 419) return; 99 // return ã§ Promise ãã§ã¼ã³ãç¹ãï¼Unhandled Promise Rejection 鲿¢ï¼ 100 return fetchFreshCsrfToken().then(function (freshToken) { 101 if (!freshToken) { 102 console.warn('[PWA] installation POST 419; fresh token unavailable'); 103 return; 104 } 105 return sendInstallationPost(url, deviceType, freshToken).then(function (retr
105yRes) { 106 // ãªãã©ã¤ã¯ 1 åã¾ã§ãå度失æãã¦ã次åã®ãã¼ã¸ãã¼ãï¼æ° sessionStorageï¼ã§å試è¡ããã 107 if (!retryRes.ok) { 108 console.warn('[PWA] installation POST retry failed with status', retryRes.status); 109 } 110 }); 111 }); 112 }).catch(function (err) { 113 // ãããã¯ã¼ã¯ã¨ã©ã¼çãæ¬¡å page load ã§åå¤å®ããã 114 console.warn('[PWA] installation POST failed', err); 115 }); 116 } 117 118 // ----------------------------------------------------- 119 // â standalone èµ·åæ¤ç¥ + GA4 ã¤ãã³ã + DB POST 120 // ----------------------------------------------------- 121 if (isStandalone()) { 122 // body ã« .pwa-standalone ã¯ã©ã¹ä»ä¸ï¼CSS ã§ãã¹ã³ããã»ã¢ã¼ãã«ãé表示ã«ããï¼ 123 // @media (display-mode: standalone) 㨠JS ãã©ã¼ã«ããã¯ã®äºéé²å¾¡ 124 if (document.body) { 125 document.body.classList.add('pwa-standalone'); 126 } else { 127 document.addEventListener('DOMContentLoaded', function () { 128 document.body.classList.add('pwa-standalone'); 129 }); 130 } 131 132 var prev = getPrevState(); 133 var isLoggedIn = window.PWA_IS_USER_LOGGED_IN === true; 134 var deviceType = detectDeviceType(); 135 var shouldPost = false; 136 137 if (!prev.initialized) { 138 // èµ·åæï¼ã»ãã·ã§ã³ååï¼: GA4 çºç« 139 sendGa4LaunchEvent(); 140 if (isLoggedIn) { 141 shouldPost = true; 142 } 143 } else if (prev.isLoggedIn === false && isLoggedIn === true) { 144 // ãã°ã¤ã³æï¼ç¶æ å¤åæ¤ç¥ï¼: GA4 ã¯çºç«ãã DB POST ã®ã¿ 145 shouldPost = true; 146 } 147 148 setState({ initialized: true, isLoggedIn: isLoggedIn }); 149 150 if (shouldPost) { 151 postInstallation(deviceType); 152 } 153 } 154 155 // ----------------------------------------------------- 156 // â¡ ãã¹ã³ããå°ç· + ã¤ã³ã¹ãã¼ã«ã¢ã¼ãã«å¶å¾¡ 157 // ï¼standalone ã§ãªãå ´åãå®è¡: ãã¹ã³ããã¯æªã¤ã³ã¹ãã¼ã«æã«ã表示ãããï¼ 158 // ----------------------------------------------------- 159 var deferredPrompt = null; 160 161 document.addEventListener('DOMContentLoaded', function () { 162 initMascot(); 163 initInstallModal(); 164 }); 165 166 // ã¨ã³ã²ã¼ã¸ã¡ã³ãé£å Popupï¼TOPï¼ããéãããã®å ¬é APIã 167 // resetStep è¾¼ã¿ã§éãããããã¹ã³ããçµç±ã§éä¸ã¾ã§é²ããç¶æ ãæ®ããªãã 168 window.openPwaInstallModal = openInstallModal; 169 170 window.addEventListener('beforeinstallprompt', function (e) { 171 // Android Chrome ç¨: prompt() ãå¾ããå¼ã¶ããã«ã¤ãã³ããä¿æ 172 e.preventDefault(); 173 deferredPrompt = e; 174 }); 175 176 window.addEventListener('appinstalled', function () { 177 // ã¤ã³ã¹ãã¼ã«å®äºãããã¢ã¼ãã«ãéãã 178 closeInstallModal(); 179 }); 180 181 function initMascot() { 182 var mascot = document.querySelector('[data-pwa-mascot]'); 183 if (!mascot) return; 184 185 // ãã¹ã³ããç»åã«æè¨ãåãè¾¼ã¾ãã¦ãããããã¿ããã§ç´æ¥ã¢ã¼ãã«ãéãï¼å¹ãåºããªãï¼ 186 var addBtn = mascot.querySelector('[data-pwa-mascot-add]'); 187 if (addBtn) { 188 addBtn.addEventListener('click', function () { 189 sendGa4Event('pwa_mascot_clicked'); 190 openInstallModal(); 191 }); 192 } 193 } 194 195 function initInstallModal() { 196 var modal = document.getElementById('pwa-install-modal'); 197 if (!modal) return; 198 199 // éãããã¿ã³ç¾¤ 200 var closers = modal.querySelectorAll('[data-pwa-modal-close]'); 201 for (var i = 0; i < closers.length; i++) { 202 closers[i].addEventListener('click', closeInstallModal); 203 } 204 205 // Androidããã¼ã ç»é¢ã«è¿½å ãããâ beforeinstallprompt ã® prompt() å¼ã¶ 206 var androidAdd = modal.querySelector('[data-pwa-modal-android-add]'); 207 if (androidAdd) { 208 androidAdd.addEventListener('click', function () { 209 sendGa4Event('pwa_install_cta_clicked'); 210 if (!deferredPrompt) { 211 // beforeinstallprompt ãæªææï¼æ¢ã¤ã³ã¹ãã¼ã« or é対å¿ï¼â éãã«éãã 212 closeInstallModal(); 213 return; 214 } 215 deferredPrompt.prompt(); 216 deferredPrompt.userChoice.finally(function () { 217 deferredPrompt = null; 218 closeInstallModal(); 219 }); 220 }); 221 } 222 223 // iOS Safari ã¬ã¤ã: ã¹ãããé²ã/æ»ã 224 var nextBtns = modal.querySelectorAll('[data-pwa-step-next]'); 225 for (var j = 0; j < nextBtns.length; j++) { 226 nextBtns[j].addEventListener('click', function () { 227 changeStep(modal, +1); 228 }); 229 } 230 var prevBtns = modal.querySelectorAll('[data-pwa-step-prev]'); 231 for (var k = 0; k < prevBtns.length; k++) { 232 prevBtns[k].addEventListener('click', function () { 233 changeStep(modal, -1); 234 }); 235 } 236 237 // ã¢ã¼ãã«ãéããç¬éã« Esc ãã¼ã§éããããã 238 document.addEventListener('keydown', function (e) { 239 if (e.key === 'Escape' && !modal.hidden) closeInstallModal(); 240 }); 241 } 242 243 function openInstallModal() { 244 var modal = document.getElementById('pwa-install-modal'); 245 if (!modal) return; 246 modal.hidden = false;
247 // æåã® stepï¼intro=0 ã¾ã㯠step=1ï¼ã«ãªã»ããï¼iOS Safari ã¬ã¤ãç¨ï¼ 248 resetStep(modal); 249 } 250 251 function closeInstallModal() { 252 var modal = document.getElementById('pwa-install-modal'); 253 if (!modal) return; 254 modal.hidden = true; 255 } 256 257 function resetStep(modal) { 258 var steps = modal.querySelectorAll('[data-pwa-step]'); 259 if (steps.length === 0) { 260 updateProgress(modal, 1); 261 return; 262 } 263 // æå°ã® stepï¼iOS ã¬ã¤ã㯠intro=0ããã以å¤ã¯ 1ï¼ãåæè¡¨ç¤ºã«ãã 264 var min = null; 265 for (var i = 0; i < steps.length; i++) { 266 var n = parseInt(steps[i].getAttribute('data-pwa-step'), 10); 267 if (isNaN(n)) continue; 268 if (min === null || n < min) min = n; 269 } 270 if (min === null) min = 1; 271 for (var k = 0; k < steps.length; k++) { 272 steps[k].hidden = (parseInt(steps[k].getAttribute('data-pwa-step'), 10) !== min); 273 } 274 updateProgress(modal, min); 275 } 276 277 function changeStep(modal, delta) { 278 var steps = modal.querySelectorAll('[data-pwa-step]'); 279 if (steps.length === 0) return; 280 // å®å¨ãã step çªå·ããæå°ã»æå¤§ãæ±ããï¼0 å§ã¾ãé£ç¶åæã«ä¾åããªãï¼ 281 var minStep = null, maxStep = null; 282 for (var i = 0; i < steps.length; i++) { 283 var sn = parseInt(steps[i].getAttribute('data-pwa-step'), 10); 284 if (isNaN(sn)) continue; 285 if (minStep === null || sn < minStep) minStep = sn; 286 if (maxStep === null || sn > maxStep) maxStep = sn; 287 } 288 if (minStep === null) return; 289 // 表示ä¸ã® step ã current ã¨ããï¼å ¨ hidden ã® DOM ç°å¸¸æã¯ minStep ã«ãã©ã¼ã«ããã¯ï¼ 290 var current = minStep; 291 for (var j = 0; j < steps.length; j++) { 292 if (!steps[j].hidden) { 293 // step="0"ï¼introï¼ãæ£ããåå¾ãããã || 1 ã¯ä½¿ããªã 294 var c = parseInt(steps[j].getAttribute('data-pwa-step'), 10); 295 if (!isNaN(c)) current = c; 296 break; 297 } 298 } 299 var next = current + delta; 300 if (next < minStep || next > maxStep) return; 301 for (var k = 0; k < steps.length; k++) { 302 steps[k].hidden = (parseInt(steps[k].getAttribute('data-pwa-step'), 10) !== next); 303 } 304 updateProgress(modal, next); 305 } 306 307 function updateProgress(modal, step) { 308 var progress = modal.querySelector('[data-pwa-progress]'); 309 // introï¼step < 1ï¼ã§ã¯ããã°ã¬ã¹ãã¼ãé ã 310 if (step < 1) { 311 if (progress) progress.hidden = true; 312 return; 313 } 314 if (progress) progress.hidden = false; 315 var label = modal.querySelector('[data-pwa-step-label]'); 316 if (label) label.textContent = 'ã¹ããã' + step + '/3'; 317 var fill = modal.querySelector('[data-pwa-progress-fill]'); 318 if (fill) fill.style.width = (step / 3 * 100).toFixed(2) + '%'; 319 } 320})();
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.