PageSourceSearch

https://portal-gameteam.vercel.app/public/portal-free-shards-claim.js?v=20260826

js portal-gameteam.vercel.app collected 2026-10-03 07:30:57 UTC 13,628 bytes, 341 lines download raw bytes

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.