1/** 2 * Claim intro free-Shards after AIR sign-in (Studio / marketing pages). 3 * Calls hub CORS endpoint; enroll cookie on .portalgaming.com. 4 * Shows confirm overlay using server filled_slots (Option A), gated to 5 * day 1 + return days until four distinct days 2â7 are met, then never again. 6 */ 7(function () { 8 var COOKIE = 'portal_hub_fs_campaign'; 9 var CONFIRM_DAY_KEY = 'portal_hub_free_shards_confirm_shown_day'; 10 var CONFIRM_RETURN_COMPLETE_KEY = 11 'portal_hub_free_shards_confirm_return_complete'; 12 var RETURN_VISIT_TARGET = 4; 13 var HUB = 14 (typeof window !== 'undefined' && window.PORTAL_HUB_ORIGIN) || 15 'https://www.portalgaming.com'; 16 17 function isPortalDomain() { 18 try { 19 return window.location.hostname.endsWith('portalgaming.com'); 20 } catch (_) { 21 return false; 22 } 23 } 24 25 function enroll() { 26 try { 27 var maxAge = 60 * 60 * 24 * 30; 28 var domain = isPortalDomain() ? '; Domain=.portalgaming.com; Secure' : ''; 29 document.cookie = 30 COOKIE + '=1; path=/; max-age=' + maxAge + '; samesite=lax' + domain; 31 localStorage.setItem(COOKIE, '1'); 32 } catch (_) {} 33 } 34 35 function claimTodayKey(userId) { 36 var day = new Date().toISOString().slice(0, 10); 37 return 'portal_hub_fs_claim_' + userId + '_' + day; 38 } 39 40 function wasConfirmShownToday() { 41 var today = new Date().toISOString().slice(0, 10); 42 try { 43 if (localStorage.getItem(CONFIRM_DAY_KEY) === today) return true; 44 var prefix = CONFIRM_DAY_KEY + '='; 45 var match = document.cookie.split('; ').find(function (c) { return c.indexOf(prefix) === 0; }); 46 if (match && decodeURIComponent(match.slice(prefix.length)) === today) { 47 try { localStorage.setItem(CONFIRM_DAY_KEY, today); } catch (_) {} 48 return true; 49 } 50 return false; 51 } catch (_) { 52 return false; 53 } 54 } 55 56 function markConfirmShownToday() { 57 var today = new Date().toISOString().slice(0, 10); 58 try { 59 localStorage.setItem(CONFIRM_DAY_KEY, today); 60 var domain = isPortalDomain() ? '; Domain=.portalgaming.com; Secure' : ''; 61 document.cookie = 62 CONFIRM_DAY_KEY + 63 '=' + 64 encodeURIComponent(today) + 65 '; path=/; max-age=' + 66 60 * 60 * 36 + 67 '; samesite=lax' + 68 domain; 69 } catch (_) {} 70 } 71 72 function ensureDay1Filled(filledSlots) { 73 var next = []; 74 for (var i = 0; i < 7; i++) next[i] = !!(filledSlots && filledSlots[i]); 75 next[0] = true; 76 return next; 77 } 78 79 /** Hub /intro already showed day-1 confirm â skip on Studio handoff. */ 80 function consumeIntroConfirmAlreadyShown() { 81 try { 82 var params = new URLSearchParams(window.location.search); 83 var fromIntro = 84 params.get('hub_source') === 'intro_free_shards_promo' || 85 params.get('fs_confirm') === '1'; 86 if (!fromIntro) return false; 87 markConfirmShownToday(); 88 return true; 89 } catch (_) { 90 return false; 91 } 92 } 93 94 function isConfirmReturnComplete() { 95 try { 96 if (localStorage.getItem(CONFIRM_RETURN_COMPLETE_KEY) === '1') return true; 97 var prefix = CONFIRM_RETURN_COMPLETE_KEY + '='; 98 return document.cookie.split('; ').some(function (c) { 99 return c === prefix + '1' || c.indexOf(prefix + '1;') === 0; 100 }); 101 } catch (_) { 102 return false; 103 } 104 } 105 106 function markConfirmReturnComplete() { 107 try { 108 localStorage.setItem(CONFIRM_RETURN_COMPLETE_KEY, '1'); 109 var domain = isPortalDomain() ? '; Domain=.portalgaming.com; Secure' : ''; 110 document.cookie = 111 CONFIRM_RETURN_COMPLETE_KEY + 112 '=1; path=/; max-age=' + 113 60 * 60 * 24 * 400 + 114 '; samesite=lax' + 115 domain; 116 } catch (_) {} 117 } 118 119 function countReturnVisitDaysFilled(filledSlots) { 120 if (!Array.isArray(filledSlots)) return 0; 121 var n = 0; 122 for (var i = 1; i < filledSlots.length; i++) { 123 if (filledSlots[i]) n += 1; 124 } 125 return n; 126 } 127 128 /** 129 * @returns {'skip'|'show'|'show_final'} 130 */ 131 function decideConfirmOpen(filledSlots) { 132 if (isConfirmReturnComplete()) return 'skip'; 133 var filled = countReturnVisitDaysFilled(filledSlots); 134 if (filled > RETURN_VISIT_TARGET) { 135 markConfirmReturnComplete(); 136 return 'skip'; 137 } 138 if (wasConfirmShownToday()) return 'skip'; 139 if (filled >= RETURN_VISIT_TARGET) return 'show_final'; 140 return 'show'; 141 } 142 143 function pipClass(on, animate, delay) { 144 var cls = on ? 'is-filled' : 'is-inactive'; 145 if (animate && on) cls += ' hub-free-shards-promo__pip--pop'; 146 return { cls: cls, delay: animate && on ? delay : null }; 147 } 148 149 function showConfirm(filledSlots, copy) { 150 if (consumeIntroConfirmAlreadyShown() || wasConfirmShownToday()) return; 151 var slots = ensureDay1Filled( 152 Array.isArray(filledSlots) ? filledSlots : [] 153 ); 154 var decision = decideConfirmOpen(slots); 155 if (decision === 'skip') return; 156 157 if (decision === 'show_final') markConfirmReturnComplete(); 158 159 var c = copy && typeof copy === 'object' ? copy : {}; 160 var title = c.title || 'Shards unlocked!'; 161 var lead = 162 c.lead || 163 'Thanks for signing in. Hereâs your login progress this week â filled days are when you claimed progress toward free Shards.'; 164 var day1Title = c.day1Title || 'Day 1'; 165 var day1Amount = c.day1Amount || '50 Shards';
166 var day1Meta = c.day1Meta || 'Account created â Day 1 reward unlocked'; 167 var returnTitle = c.returnTitle || 'Days 2â7'; 168 var returnAmount = c.returnAmount || '250 Shards'; 169 var returnMeta = 170 c.returnMeta || 'Log in on four different days by day 7 for +250 Shards'; 171 var footnote = 172 c.footnote || 173 'Skipped days stay empty. Keep logging in on new days this week to finish the reward.'; 174 var cta = c.cta || 'Got it'; 175 176 if (document.getElementById('portal-fs-confirm-root')) return; 177 178 var style = document.createElement('style'); 179 style.textContent = 180 '#portal-fs-confirm-root{position:fixed;inset:0;z-index:99999;display:flex;align-items:center;justify-content:center;padding:1rem;font-family:Inter,system-ui,sans-serif}' + 181 '#portal-fs-confirm-root .ov{position:absolute;inset:0;border:0;background:rgba(0,0,0,.75);cursor:pointer}' + 182 '#portal-fs-confirm-root .panel{position:relative;z-index:1;width:min(420px,100%);overflow:hidden;border:1px solid rgba(255,255,255,.1);border-radius:12px;background:#12121a;color:#fff;box-shadow:0 24px 80px rgba(0,0,0,.55)}' + 183 '#portal-fs-confirm-root .body{padding:1.25rem;display:flex;flex-direction:column;gap:1rem}' + 184 '#portal-fs-confirm-root h2{margin:0;font-size:1.25rem;text-align:center}' + 185 '#portal-fs-confirm-root .lead{margin:0;color:rgba(255,255,255,.6);font-size:.875rem;text-align:center;line-height:1.5}' + 186 '#portal-fs-confirm-root .tiers{display:grid;grid-template-columns:0.45fr 0.55fr;border:1px solid rgba(255,255,255,.1);border-radius:12px;overflow:hidden}' + 187 '#portal-fs-confirm-root .tier{text-align:center;padding-bottom:.75rem}' + 188 '#portal-fs-confirm-root .tier+.tier{border-left:1px solid rgba(255,255,255,.1)}' + 189 '#portal-fs-confirm-root .head{background:#c3b5ff;color:#0a0a0e;font-size:.72rem;font-weight:700;padding:.55rem}' + 190 '#portal-fs-confirm-root .head.alt{background:rgba(195,181,255,.22);color:#e8e0ff}' + 191 '#portal-fs-confirm-root .pips{display:flex;justify-content:center;gap:.18rem;padding:1rem .3rem .65rem;flex-wrap:nowrap}' + 192 '#portal-fs-confirm-root .pips span{display:inline-flex;align-items:center;justify-content:center;width:1.65rem;height:1.65rem;border-radius:999px;font-size:.7rem;font-weight:700}' + 193 '#portal-fs-confirm-root .pips span.is-filled{background:linear-gradient(145deg,#7c6cff,#60a5fa);color:#fff}' + 194 '#portal-fs-confirm-root .pips span.is-inactive{background:#1e1e2a;color:rgba(255,255,255,.32);border:1px solid rgba(255,255,255,.12)}' + 195 '#portal-fs-confirm-root .amt{margin:0;color:#c3b5ff;font-weight:700;font-size:.95rem}' + 196 '#portal-fs-confirm-root .meta{margin:.25rem .5rem 0;color:rgba(255,255,255,.55);font-size:.72rem;line-height:1.35}' + 197 '#portal-fs-confirm-root .foot{margin:0;color:rgba(255,255,255,.45);font-size:.75rem;text-align:center}' + 198 '#portal-fs-confirm-root .cta{appearance:none;border:0;border-radius:4px;background:#c3b5ff;color:#0a0a0e;font-weight:700;padding:.75rem 1rem;cursor:pointer}' + 199 '#portal-fs-confirm-root .close{position:absolute;top:.55rem;right:.55rem;width:2rem;height:2rem;border:0;border-radius:999px;background:rgba(0,0,0,.45);color:#fff;font-size:1.25rem;cursor:pointer}' + 200 '@keyframes portalFsPop{0%{transform:scale(.55)}55%{transform:scale(1.14)}100%{transform:scale(1)}}' + 201 '#portal-fs-confirm-root .hub-free-shards-promo__pip--pop{animation:portalFsPop .5s cubic-bezier(.22,1,.36,1) both}'; 202 203 var root = document.createElement('div'); 204 root.id = 'portal-fs-confirm-root'; 205 root.setAttribute('role', 'presentation'); 206 207 function close() { 208 markConfirmShownToday(); 209 if (root.parentNode) root.parentNode.removeChild(root); 210 if (style.parentNode) style.parentNode.removeChild(style); 211 } 212 213 var day1 = true; 214 var returnHtml = ''; 215 for (var i = 1; i < 7; i++) { 216 var p = pipClass(!!slots[i], true, 0.12 + (i - 1) * 0.14); 217 returnHtml += 218 '<span class="' + 219 p.cls + 220 '"' + 221 (p.delay != null ? ' style="animation-delay:' + p.delay + 's"' : '') + 222 '>' + 223 (i + 1) + 224 '</span>'; 225 } 226 var d1 = pipClass(day1, true, 0.12); 227 228 root.innerHTML = 229 '<button type="button" class="ov" aria-label="Close"></button>' + 230 '<div class="panel" role="dialog" aria-modal="true" aria-label="Shards unlocked">' + 231 '<button type="button" class="close" aria-label="Close">Ã</button>' + 232 '<div class="body">' + 233 '<h2>' + title + '</h2>' + 234 '<p class="lead">' + lead + '</p>' + 235 '<div class="tiers">' + 236 '<div class="tier"><div class="head">' + day1Title + '</div><div class="pips"><span class="' + 237 d1.cls + 238 '"' + 239 (d1.delay != null ? ' style="animation-delay:' + d1.delay + 's"' : '') + 240 '>1</span></div><p class="amt">' + day1Amount + '</p><p class="meta">' + day1Meta + '</p></div>' + 241 '<div class="tier"><div class="head alt">' + returnTitle + '</div><div class="pips">' + 242 returnHtml + 243 '</div><p class="amt">' + returnAmount + '</p><p class="meta">' + returnMeta + '</p></div>' + 244 '</div>' + 245 '<p class="foot">' + footnote + '</p>' + 246 '<button type="button" class="cta">' + cta + '</button>' + 247 '</div></div>'; 248 249 document.head.appendChild(style); 250 document.body.appendChild(root); 251 root.querySelector('.ov').addEventListener('click', close); 252 root.querySelector('.close').addEventListener('click', close); 253 root.querySelector('.cta').addEventListener('click', close); 254 } 255 256 function claim(session, options) { 257 options = options || {}; 258 // Hub owns the confirm overlay. Products still claim silently. 259 options = Object.assign({}, options, { showConfirm: false }); 260 if ( 261 window.PortalAirSession && 262 typeof window.PortalAirSession.isHubEmbed === 'function' && 263 window.PortalAirSession.isHubEmbed() 264 ) { 265 return Promise.resolve(null); 266 } 267 try { 268 var params = new URLSearchParams(window.location.search); 269 if (params.get('hub_embed') === '1' || params.get('hub_preview') === '1') { 270 return Promise.resolve(null); 271 } 272 } catch (_) {} 273 if (!session || !session.token) return Promise.resolve(null); 274 enroll(); 275 if ( 276 consumeIntroConfirmAlreadyShown() || 277 isConfirmReturnComplete() || 278 wasConfirmShownToday() 279 ) { 280 options = Object.assign({}, options, { showConfirm: false }); 281 } 282 var userId = session.id || ''; 283 try { 284 if (userId && sessionStorage.getItem(claimTodayKey(userId)) === '1') { 285 return Promise.resolve(null); 286 } 287 if (userId) sessionStorage.setItem(claimTodayKey(userId), '1'); 288 } catch (_) {} 289 290 var url = String(HUB).replace(/\/$/, '') + '/api/hub-free-shards/claim'; 291 return fetch(url, { 292 method: 'POST', 293 headers: { 294 'Content-Type': 'application/json', 295 Authorization: 'Bearer ' + session.token, 296 }, 297 body: JSON.stringify({ 298 enroll: true, 299 locale: 300 (function () { 301 try { 302 var m = document.cookie.split('; ').find(function (c) { 303 return c.indexOf('portal_locale=') === 0; 304 }); 305 if (m) return decodeURIComponent(m.slice('portal_locale='.length)); 306 } catch (_) {} 307 return (navigator.language || 'en'); 308 })(), 309 }), 310 keepalive: true, 311 }) 312 .then(function (res) { 313 return res.json().catch(function () { 314 return {}; 315 }); 316 }) 317 .then(function (data) { 318 if (options.showConfirm !== false) { 319 var slots = data && data.filled_slots; 320 var serverCopy = data && data.copy; 321 window.setTimeout(function () { 322 showConfirm(slots, serverCopy); 323 }, 600); 324 } else if (data && Array.isArray(data.filled_slots)) { 325 // Catch-up complete flag when confirm UI is suppressed. 326 decideConfirmOpen(data.filled_slots); 327 } 328 return data; 329 }) 330 .catch(function (err) { 331 console.warn('[portal-free-shards] claim failed', err); 332 return null; 333 }); 334 } 335 336 window.PortalFreeShards = { 337 enroll: enroll, 338 claim: claim, 339 showConfirm: showConfirm, 340 }; 341})();
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.