PageSourceSearch

https://isb.edu.vn/wp-content/plugins/google-onetap-capture/assets/js/onetap.js?ver=1789632646

js isb.edu.vn collected 2026-09-24 09:55:50 UTC 10,250 bytes, 229 lines download raw bytes

1/**
2 * google-onetap-capture — khởi tạo Google One Tap và lời chào trên top-bar.
3 *
4 * Phụ thuộc: core.js (window.GOT), phone-popup.js (GOT.showPhonePopup),
5 *            thư viện GSI của Google (accounts.google.com/gsi/client).
6 */
7(function (w) {
8  'use strict';
9
10  var G = w.GOT;
11  if (!G) return;
12
13  var cfg = G.cfg;
14
15  // Bấm X xong bao lâu thì mời đăng nhập lại. Để 0 thì prompt nhảy ra tức thì,
16  // trông như trang bị lỗi; vài giây là đủ để user hiểu mình vừa thoát xong.
17  var RELOGIN_MS = 3000;
18
19  // Decode payload JWT chỉ để hiển thị (tên/ảnh) — không phải để xác thực
20  function decodePayload(jwt) {
21    try {
22      var p = jwt.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
23      while (p.length % 4) p += '=';
24      var bytes = Uint8Array.from(atob(p), function (c) { return c.charCodeAt(0); });
25      return JSON.parse(new TextDecoder('utf-8').decode(bytes));
26    } catch (e) { return {}; }
27  }
28
29  /**
30   * Tìm (hoặc tạo) chỗ để render lời chào.
31   * mode:
32   *   'host'     -> theme tự cung cấp <div id="got-user-greeting">
33   *   'nav'      -> chèn vào top-bar, ngay cạnh bộ chọn ngôn ngữ (.choose-lang)
34   *   'floating' -> badge nổi góc phải dưới (dự phòng khi không có top-bar)
35   */
36  function resolveHost() {
37    // 1) Theme có sẵn <div id="got-user-greeting"></div> thì render vào đó
38    var host = document.getElementById('got-user-greeting');
39    // 2) Đã render trước đó -> tái sử dụng đúng chỗ cũ
40    if (!host) host = document.getElementById('got-greeting-badge');
41    if (host) {
42      return {
43        el: host,
44        mode: host.id === 'got-greeting-badge' ? (host.tagName === 'LI' ? 'nav' : 'floating') : 'host'
45      };
46    }
47
48    // 3) Chèn thành 1 <li> nav mới, NGAY SAU <li> chứa bộ chọn ngôn ngữ,
49    //    để nằm NGANG HÀNG, bên phải "Vietnam" (các <li> trong top-bar xếp ngang).
50    var lang = document.querySelector('.choose-lang');
51    if (lang) {
52      host = document.createElement('li');
53      host.id = 'got-greeting-badge';
54      host.className = 'html custom got-badge--nav';
55      var langLi = lang.closest('li');
56      var navUl  = langLi ? langLi.parentNode : null;
57      if (navUl && langLi) navUl.insertBefore(host, langLi.nextSibling);
58      else { host.style.marginLeft = '12px'; lang.appendChild(host); }
59      return { el: host, mode: 'nav' };
60    }
61
62    // 4) Dự phòng: badge nổi góc phải dưới.
63    //    bottom:90px để nổi LÊN TRÊN bong bóng chat/dock thường nằm sát đáy phải;
64    //    z-index tối đa để không bị widget khác đè.
65    host = document.createElement('div');
66    host.id = 'got-greeting-badge';
67    host.className = 'got-badge--floating';
68    document.body.appendChild(host);
69    return { el: host, mode: 'floating' };
70  }
71
72  function renderGreeting(u) {
73    if (!u) return;
74
75    var h = resolveHost(), host = h.el, mode = h.mode;
76    var xicon = '<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">'
77      + '<path d="M1.33333 12C0.966667 12 0.652889 11.8696 0.392 11.6087C0.131111 11.3478 0.000444444 11.0338 0 10.6667V1.33333C0 0.966667 0.130667 0.652889 0.392 0.392C0.653333 0.131111 0.967111 0.000444444 1.33333 0H6V1.33333H1.33333V10.6667H6V12H1.33333ZM8.66667 9.33333L7.75 8.36667L9.45 6.66667H4V5.33333H9.45L7.75 3.63333L8.66667 2.66667L12 6L8.66667 9.33333Z" fill="currentColor"/>'
78      + '</svg>';
79
80    // Style ở assets/css/onetap.css (#got-greeting-badge)
81    host.innerHTML =
82        '<span class="got-hello">Xin chào, <strong></strong></span>'
83      + '<a href="#" id="got-forget" class="got-logout" title="Thoát" aria-label="Thoát">' + xicon + '</a>';
84
85    host.querySelector('strong').textContent = (u.name || u.email || 'bạn'); // chèn tên an toàn (chống XSS)
86
87    var f = document.getElementById('got-forget');
88    if (f) f.addEventListener('click', function (ev) {
89      ev.preventDefault();
90      G.forgetUser();
91
92      // KHÔNG gọi disableAutoSelect() ở đây.
93      //
94      // initialize() bên dưới đã truyền auto_select:false nên tự-động-đăng-nhập
95      // vốn đã tắt — gọi thêm không được gì. Đổi lại, ở chế độ FedCM nó ghi một
96      // dấu ở tầng TRÌNH DUYỆT (preventSilentAccess), nằm ngoài cookie của site
97      // nên xoá cookie không dọn được, và đó là điểm khác biệt duy nhất giữa
98      // "vào trang lần đầu" (chạy được) với "sau khi thoát" (FedCM trả về
99      // NetworkError: Error retrieving a token).
100      //
101      // Quên user ở phía mình là đủ: lần sau prompt hiện lại y như khách mới.
102
103      // Gỡ lời chào. host mode: theme sở hữu phần tử -> chỉ xoá nội dung.
104      if (mode !== 'host' && host.parentNode) host.parentNode.removeChild(host);
105      else host.innerHTML = '';
106
107      // Rồi mời đăng nhập lại NGAY trên trang hiện tại.
108      // Plugin này để thu lead chứ không phải để đăng nhập: bỏ trống chỗ đăng
109      // nhập sau khi bấm X là mất luôn đường nhận diện khách, mà user cũng
110      // không có cách nào quay lại ngoài việc tự F5.
111      setTimeout(armPrompt, RELOGIN_MS);
112    });
113  }
114
115  function note() {
116    if (!cfg.consent || document.getElementById('got-consent-note')) return;
117    var d = document.createElement('div');
118    d.id = 'got-consent-note';   // style ở assets/css/onetap.css
119    d.textContent = cfg.consent;
120    document.body.appendChild(d);
121    setTimeout(function () {
122      if (d && d.parentNode) {
123        d.classList.add('got-note--out');
124        setTimeout(function () { if (d.parentNode) d.parentNode.removeChild(d); }, 400);
125      }
126    }, 9000);
127  }
128
129  function handle(resp) {
130    if (!resp || !resp.credential) return;
131    fetch(cfg.captureUrl, {
132      method: 'POST',
133      headers: { 'Content-Type': 'application/json' },
134      body: JSON.stringify({ credential: resp.credential })
135    })
136      .then(function (r) { return r.json(); })
137      .then(function (d) {
138        if (!d || !d.ok) { if (w.console) w.console.warn('[GOT] capture:', d); return; }
139        var p = decodePayload(resp.credential);
140        var u = { name: p.name || d.name || '', email: p.email || '', picture: p.picture || '', hp: !!d.has_phone };
141        G.setUser(u);
142        var n = document.getElementById('got-consent-note');
143        if (n && n.parentNode) n.parentNode.removeChild(n);
144        renderGreeting(u);
145
146        // Server đã có SĐT (kể cả do máy khác nhập trước đó) -> im lặng, không hỏi lại.
147        if (cfg.phoneOn && !d.has_phone && d.ticket && !G.getCookie(G.LATER)) {
148          G.showPhonePopup(d.ticket);
149        }
150      })
151      .catch(function (e) { if (w.console) w.console.warn('[GOT]', e); });
152  }
153
154  function startPrompt(nonce) {
155    if (!w.google || !w.google.accounts || !w.google.accounts.id) {
156      return setTimeout(function () { startPrompt(nonce); }, 300);
157    }
158    // Trong MỘT lần tải trang, sau khi GSI đã trả credential thì prompt() gọi
159    // lần nữa bị bỏ qua im lặng. cancel() đóng phiên UI cũ để initialize() bên
160    // dưới bắt đầu lại từ đầu. Thiếu dòng này thì bấm X xong dù có gọi lại
161    // prompt() cũng không hiện gì.
162    try { w.google.accounts.id.cancel(); } catch (e) {}
163
164    w.google.accounts.id.initialize({
165      client_id: cfg.clientId,
166      callback: handle,
167      nonce: nonce,
168      use_fedcm_for_prompt: true,
169      auto_select: false,
170      cancel_on_tap_outside: false
171    });
172    w.google.accounts.id.prompt();
173    note();
174  }
175
176  /**
177   * Lấy nonce TƯƠI rồi bật prompt.
178   *
179   * Nonce lấy mới mỗi lần chứ không dùng lại: server đối chiếu nonce trong JWT,
180   * và endpoint không-cache là cách duy nhất đúng khi site bật full-page cache.
181   * Dùng chung cho cả lúc vào trang lẫn lúc bật lại sau khi bấm X.
182   */
183  function armPrompt() {
184    fetch(cfg.nonceUrl, { headers: { Accept: 'application/json' }, cache: 'no-store' })
185      .then(function (r) { return r.json(); })
186      .then(function (d) { startPrompt(d && d.nonce ? d.nonce : ''); })
187      .catch(function () {});
188  }
189
190  // Top-bar (chứa "Vietnam" + lời chào) mặc định bị .hide-for-medium ẩn ở <=849px.
191  // Cho hiện lại RIÊNG cột phải trên mobile, và chỉ chừa mục ngôn ngữ + lời chào.
192  function showTopbarOnMobile() {
193    var lang = document.querySelector('.choose-lang');
194    if (!lang) return;
195    var col = lang.closest('.flex-col');
196    if (col) col.classList.add('got-show-mobile');
197    // #top-bar (.header-top) TỰ NÓ có .hide-for-medium -> phải un-hide cả wrapper,
198    // nếu không thì các cột con dù bỏ ẩn vẫn nằm trong cha display:none.
199    var bar = lang.closest('.header-top');
200    if (bar) bar.classList.add('got-show-mobile-bar');
201    // Các rule @media tương ứng nằm ở assets/css/onetap.css — ở đây chỉ gắn class.
202  }
203
204  function init() {
205    showTopbarOnMobile();   // luôn chạy: "Vietnam" hiện trên mobile kể cả khi chưa đăng nhập
206    G.captureUtm();         // chốt UTM ngay trang đầu, trước khi user đi tiếp
207
208    // Bản 1.4.1 trở về trước từng đặt cookie chặn 30 ngày sau khi bấm X, khiến
209    // đăng xuất xong là không đăng nhập lại được. Xoá nếu trình duyệt còn giữ.
210    if (G.getCookie(G.OPTOUT)) G.delCookie(G.OPTOUT);
211
212    var u = G.getUser();
213    if (u) {
214      renderGreeting(u);
215      // Đã có SĐT, hoặc vừa đóng popup -> xong, không cần prompt nữa.
216      // Ngược lại rơi xuống dưới chạy prompt để lấy ticket mới cho popup SĐT.
217      if (!cfg.phoneOn || u.hp || G.getCookie(G.LATER)) return;
218    }
219
220    armPrompt();
221  }
222
223  // Hàm phụ cho việc test/gỡ lỗi ngoài luồng tự chạy
224  G.renderGreeting = renderGreeting;
225  G.init = init;
226
227  if (document.readyState === 'complete') init();
228  else w.addEventListener('load', init);
229})(window);

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.