class ApiClient { static productId = ''; static siteId = ''; static ecid = ''; static eligibilityEndpoint; static eligibilityQuery; static eligibilityResponse; static submitEndpoint; static submitData; static subscribeEndpoint; static subscribeData; static responseTime = 0; static PARTNER = 'cpa-web'; static API_ENDPOINT = `/store-ga.appsclub.com/partners/api/v2.1.1/${this.PARTNER}/`; static PRE_PROD_ENDPOINT = `/store-ga.appsclub.com/partners/api/pre-production/v2.1.1/${this.PARTNER}/`; static getEndpoint() { return '/' + (this.basDev ? this.PRE_PROD_ENDPOINT : this.API_ENDPOINT); } static init(siteId, productId, useProductId, ecid) { this.siteId = siteId; this.productId = useProductId ? productId : ''; this.ecid = ecid; this.basDev = (new URLSearchParams(window.location.search)).get('bas'); } static eligibility() { const params = new URLSearchParams(window.location.search); const query = new URLSearchParams(); query.append('utm_source', params.get('utm_source') ?? ''); query.append('utm_campaign', params.get('utm_campaign') ?? ''); query.append('click_id', params.get('click_id') ?? ''); query.append('locale', document.documentElement.lang); query.append('site_id', this.siteId); query.append('css_selector', 'section button'); // query.append('ecid', this.ecid); this.eligibilityEndpoint = this.getEndpoint() + `eligibility/${this.productId}`; this.eligibilityQuery = Object.fromEntries(query.entries()); const apiStarted = Date.now(); return new Promise((success, fail) => { fetch(this.eligibilityEndpoint + '?' + query.toString(), { method: 'GET', headers: { Ecid: this.ecid, }, }) .then(response => response.json()) .then(data => { this.responseTime = Date.now() - apiStarted; this.eligibilityResponse = data; if (data.ecid) { this.ecid = data.ecid; } success(data); }) .catch(error => { fail(typeof error === 'string' ? 'Error checking eligibility' + error : error); }); }); } static submitMsisdn(msisdn) { if (!msisdn) { this.submitData = null; this.submitEndpoint = null; return Promise.reject('missing phone number'); } this.submitData = { sessionId: this.eligibilityResponse.sessionId, msisdn: msisdn, locale: document.documentElement.lang, ecid: this.ecid, }; const apiStarted = Date.now(); this.submitEndpoint = this.getEndpoint() + `send-otp/${this.productId}`; return new Promise((success, fail) => { fetch(this.submitEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', Ecid: this.ecid, }, body: JSON.stringify(this.submitData) }) .then(response => response.json()) .then(data => { this.responseTime = Date.now() - apiStarted; if (data.ecid) { this.ecid = data.ecid; } success(data); }) .catch(error => { fail(typeof error === 'string' ? 'Error send-otp' + error : error); }); }); } static subscribe(pin) { if (!pin) { this.subscribeData = null; this.subscribeEndpoint = null; return Promise.reject('missing phone pin code'); } this.subscribeData = { sessionId: this.eligibilityResponse.sessionId, pin: pin, locale: document.documentElement.lang, ecid: this.ecid, }; const apiStarted = Date.now(); this.subscribeEndpoint = this.getEndpoint() + `subscribe/${this.productId}`; return new Promise((success, fail) => { fetch(this.subscribeEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', Ecid: this.ecid, }, body: JSON.stringify(this.subscribeData) }) .then(response => response.json()) .then(data => { console.log(data); this.responseTime = Date.now() - apiStarted; if (data.ecid) { this.ecid = data.ecid; } success(data); }) .catch(error => { fail(typeof error === 'string' ? 'Error subscribing' + error : error); }); }); } } class InputController { constructor(input, onComplete) { this.input = input; this.onComplete = typeof onComplete === 'function' ? onComplete : () => {}; this.mask = (input.placeholder || '').trim(); this.hasMask = /[xX*_]/.test(this.mask); this.accessCode = input.dataset.accessCode ?? '0'; this.countryCode = (input.dataset.countryCode ?? '').trim() || null; this.templateSrc = input.dataset.template || input.dataset.msisdnTemplate; this.type = input.type; this.events = []; this.prevDigits = ''; this.intlRe = this.#toRegExp(this.templateSrc); const natInfo = this.#deriveNationalInfo(this.intlRe, this.countryCode); this.nationalFullLen = natInfo.len; this.allowedFirstSet = natInfo.firstSet; this.maskSlots = this.hasMask ? Array.from(this.mask).filter(ch => this.#isSlot(ch)).length : null; this.isPinMode = !this.countryCode && (this.type === 'number' || (/^\d+$/.test(this.mask))); this._onInput = this.#onInput.bind(this); this.init(); } init() { this.input.addEventListener('input', this._onInput); } #toRegExp(tpl) { if (!tpl) return null; if (tpl instanceof RegExp) return tpl; if (typeof tpl !== 'string') return null; const m = tpl.match(/^([#/~|`])(.+)\1([gimsuy]*)$/); try { if (m) return new RegExp(m[2], m[3] || ''); return new RegExp(tpl); } catch { return null; } } #onlyDigits(s) { return (s || '').replace(/\D+/g, ''); } #isSlot(ch) { return ch === 'x' || ch === 'X' || ch === '*' || ch === '_'; } #applyMask(maskStr, digitStr) { const out = []; let di = 0; const mArr = Array.from(maskStr); for (let i = 0; i < mArr.length; i++) { const ch = mArr[i]; if (this.#isSlot(ch)) { if (di < digitStr.length) out.push(digitStr[di++]); else break; } else { if (di < digitStr.length) out.push(ch); else break; } } return out.join(''); } #deriveNationalInfo(intlRe, countryCode) { const info = { len: null, firstSet: null }; if (!intlRe || !countryCode) return info; let src = intlRe.source; src = src.replace(/^\^/, '').replace(/\$$/, ''); src = src.replace(/^\(\?:/, '('); if (src.startsWith(countryCode)) { src = src.slice(countryCode.length); } else { const ccGroup = new RegExp(`^\\(\\?:?${countryCode}\\)`); if (ccGroup.test(src)) src = src.replace(ccGroup, ''); } if (src.startsWith('[')) { const cls = src.match(/^\[([^\]]+)\]/); if (cls) { const set = new Set(); const body = cls[1]; body.split('').forEach((ch, i, arr) => { if (ch === '-' && i > 0 && i < arr.length - 1) { const start = arr[i - 1], end = arr[i + 1]; if (/\d/.test(start) && /\d/.test(end)) { for (let d = +start; d <= +end; d++) set.add(String(d)); } } else if (/\d/.test(ch)) { set.add(ch); } }); if (set.size) info.firstSet = set; } } else if (/^\d/.test(src)) { info.firstSet = new Set([src[0]]); } let rest = src; if (/[+*?]|\{\d+,\d*\}/.test(rest)) return info; // variable length -> leave null let len = 0; rest = rest.replace(/\\d\{(\d+)\}/g, (_, n) => { len += parseInt(n,10); return ''; }); const loneD = (rest.match(/\\d/g) || []).length; len += loneD; rest = rest.replace(/\\d/g,''); const classes = (rest.match(/\[[^\]]+\]/g) || []).length; len += classes; rest = rest.replace(/\[[^\]]+\]/g,''); const litDigits = (rest.match(/\d/g) || []).length; len += litDigits; info.len = len || null; return info; } #emit(detail) { this.events.push(detail); window.dispatchEvent(new CustomEvent('inputChanged', { detail })); } #setFormat(valueDigits) { let digits = valueDigits; if (this.hasMask && typeof this.maskSlots === 'number') { digits = digits.slice(0, this.maskSlots); } this.input.value = this.hasMask ? this.#applyMask(this.mask, digits) : digits; } set(value) { this.input.value = value; this.#onInput({ inputType: 'insertFromPaste', }); console.log(value); } #onInput(ev) { const raw = this.input.value; const rawTrim = raw.trim(); const inputType = ev && typeof ev.inputType === 'string' ? ev.inputType : ''; const looksLikePaste = inputType === 'insertFromPaste' || (this.prevDigits && this.#onlyDigits(raw).length - this.prevDigits.length > 1); if (this.isPinMode) return this.#handlePin(rawTrim, looksLikePaste); let digits = this.#onlyDigits(rawTrim); if (looksLikePaste) { const cc = this.countryCode || ''; const hadPlus = rawTrim.startsWith('+'); const had00 = rawTrim.startsWith('00'); const afterPrefix = hadPlus ? this.#onlyDigits(rawTrim.slice(1)) : had00 ? this.#onlyDigits(rawTrim.slice(2)) : digits; const matchesIntl = (s) => this.intlRe ? this.intlRe.test(s) : false; if ((hadPlus || had00) && matchesIntl(afterPrefix)) { this.#emit({ type: 'tel', action: 'pastePrefixInternational', prefix: hadPlus ? '+' : '00' }); if (cc && afterPrefix.startsWith(cc)) { digits = afterPrefix.slice(cc.length); } else { digits = afterPrefix; } } else if (cc && digits.startsWith(cc) && matchesIntl(digits)) { this.#emit({ type: 'tel', action: 'pasteInternational' }); digits = digits.slice(cc.length); } else { if (this.nationalFullLen) { if (this.accessCode && digits.length === this.nationalFullLen + 1 && digits.startsWith(this.accessCode)) { this.#emit({ type: 'tel', action: 'pasteNative' }); digits = digits.slice(this.accessCode.length); } else if (digits.length === this.nationalFullLen) { this.#emit({ type: 'tel', action: 'pasteNumber' }); } } } } else { if (digits.length >= 1 && this.accessCode && digits[0] === this.accessCode) { if (!this.prevDigits) { this.#emit({type: 'tel', action: 'accessCode'}); } digits = digits.slice(1); } if (digits.length >= 1 && this.allowedFirstSet && !this.allowedFirstSet.has(digits[0])) { digits = digits.slice(1); } } if (this.nationalFullLen != null) { digits = digits.slice(0, this.nationalFullLen); } if (this.hasMask && typeof this.maskSlots === 'number') { digits = digits.slice(0, this.maskSlots); } this.#setFormat(digits); const nationalForIntl = digits; const intlCandidate = (this.countryCode || '') + nationalForIntl; if (this.intlRe && this.intlRe.test(intlCandidate)) { const outEvents = this.events.slice(); this.events.length = 0; this.onComplete(intlCandidate, outEvents); } this.prevDigits = this.#onlyDigits(this.input.value); } #handlePin(rawTrim, looksLikePaste) { let digits = this.#onlyDigits(rawTrim); let pinLen = null; if (this.intlRe) { const m = this.intlRe.source.match(/^\^\\d\{(\d+)\}\$$/); if (m) pinLen = parseInt(m[1], 10); } if (pinLen != null) { digits = digits.slice(0, pinLen); } if (this.hasMask && typeof this.maskSlots === 'number') { digits = digits.slice(0, this.maskSlots); } this.#setFormat(digits); if (looksLikePaste) { this.#emit({ type: 'pin', action: 'paste' }); } if (this.intlRe && this.intlRe.test(digits)) { const outEvents = this.events.slice(); this.events.length = 0; this.onComplete(digits, outEvents); } this.prevDigits = this.#onlyDigits(this.input.value); } } class Interface { onClick = null; inactive = false; onWakeUp = null; constructor(selector, visibility = false) { this.node = document.querySelector(selector); this.display = window.getComputedStyle(this.node).display; this.eventClick = this.getData('eventClick'); if (!visibility) { this.hide(); } this.node.style.visibility = 'visible'; } getData(name, defaultValue = null) { return this.node.dataset[name] ?? defaultValue; } setAwake(onWakeUp) { if (!this.onWakeUp) { this.onWakeUp = onWakeUp; window.addEventListener('wakeUp' + this.constructor.name, this.executeOnWakeUp.bind(this)); } } executeOnWakeUp(e) { this.onWakeUp(e.detail); } hide() { this.node.style.display = 'none'; } show() { this.node.style.display = this.display; } setInactive(inactive = true) { this.inactive = inactive; if (inactive) { this.node.classList.add('inactive'); } else { this.node.classList.remove('inactive'); } } setOnClick(onClick) { if (typeof onClick === 'function' && !this.onClick) { this.onClick = onClick; this.node.addEventListener('click', ()=>{ console.log(this.inactive, onClick, this.eventClick); if (!this.inactive) { PlatformLog.gtmEvent(this.eventClick); this.onClick(); } }); } } wakeUp(name, data) { window.dispatchEvent(new CustomEvent(`wakeUp${name}`, {detail: data})); } static onError(message) { console.error(message); WaitingOverlay.hide(); Notify.show(typeof message === 'string' ? message : JSON.stringify(message)); } } class Notify { static NOTIFY_CLASS = "hold-notify"; static DELAY = 3000; static timeout = null; static cache = ''; static init(selector) { this.node = document.querySelector(selector); } static show(message, delay = 0) { this.cache = this.node.innerHTML; this.node.innerHTML = message; this.node.classList.add(this.NOTIFY_CLASS); if (this.timeout) { clearTimeout(this.timeout); } this.timeout = setTimeout(this.#restore.bind(this), delay ? delay * 1000 : this.DELAY); } static #restore() { this.node.innerHTML = this.cache; this.node.classList.remove(this.NOTIFY_CLASS); } } class PlatformLog { static initRequired = true; static confSource = document.querySelector('[data-base-path-href]'); static baseHref = this.confSource.dataset.basePathHref ?? ''; static ENDPOINT = this.baseHref + 'register/update'; static #gtmEventLog = []; static send( eventName, detail, ecid = null, logAcquisition = null, ) { let headers = { 'Content-Type': 'application/json' }; if (Object.keys(PlatformLog.#gtmEventLog).length) { detail['gtm_events'] = PlatformLog.#gtmEventLog; } if (ecid) { detail['ecid'] = ecid; headers['ecid'] = ecid; } if (UnifiedDirect.msisdn) { detail['msisdn'] = UnifiedDirect.msisdn; } if (UnifiedDirect.pin) { detail['pin'] = UnifiedDirect.pin; } let data = { event: eventName, detail: detail, mode: 'merge', logAcquisition: logAcquisition, }; fetch(this.ENDPOINT, { method: 'POST', headers: headers, body: JSON.stringify(data) }) .catch(error => console.error('log submission failed for ' + eventName, error)); } static gtmEvent(detail) { if (detail && typeof dataLayer !== 'undefined') { PlatformLog.#gtmEventLog.push(detail); dataLayer.push(typeof detail === 'string' ? {event: detail} : detail ) } } } class Popup { confSource = document.querySelector('[data-base-path-href]'); baseHref = this.confSource.dataset.basePathHref ?? ''; TNC_ENDPOINT = this.baseHref + 'static/tnc/'; container; content; storage = []; constructor() { this.init(); this.initTnc(); this.initContent(); window.addEventListener('DOMUpdated', this.initContent.bind(this), false); } init() { const container = document.createElement("div"); const shadow = document.createElement("div"); const box = document.createElement("div"); const content = document.createElement("div"); const button = document.createElement("button"); const buttonSource = document.getElementById("tnc_back"); container.className = "popup-container"; content.className = "popup-content"; shadow.className = 'popup-shadow'; shadow.addEventListener('click', this.hide.bind(this)); button.innerHTML = buttonSource ? buttonSource.innerHTML : 'back'; button.addEventListener("click", this.hide.bind(this)); box.className = 'popup-box'; box.appendChild(content); box.appendChild(button); container.appendChild(shadow); container.appendChild(box); document.body.appendChild(container); this.container = container; this.content = content; } show() { this.container.style.display = "flex"; } hide() { this.container.style.display = "none"; this.content.innerHTML = ''; this.content.classList.remove("short-content"); } #clickType(contentType, endpoint) { const lang = document.documentElement.lang; this.show(); this.content.innerHTML = this.storage[contentType] ?? 'loading...'; if (this.storage[contentType]) { return; } fetch(endpoint + '?lang=' + lang) .then(res => res.text()) .then((response) => { this.content.innerHTML = response; this.storage[contentType] = response; }) .catch((error) => { console.error(`${contentType} load failed!`, error); }); } #clickText(endpoint) { this.content.innerHTML = endpoint.innerHTML; this.content.classList.add("short-content"); this.show(); } initTnc() { document.querySelectorAll(".tnc").forEach((tnc) => { tnc.addEventListener("click", this.#clickType.bind(this, 'tnc', this.TNC_ENDPOINT)); }); } initContent() { document.querySelectorAll(".content").forEach((el) => { let endpoint = ''; let source = null; let type = el.dataset.content || ''; if (type && type.slice(-6) === 'policy') { endpoint = this.baseHref + 'assets/' + type; } else { endpoint = el.dataset.endpoint ?? null; source = el.dataset.source ?? null; } if (endpoint) { el.addEventListener("click", this.#clickType.bind(this, type, endpoint)); } else if (source) { const sourceEl = document.getElementById(source); if (sourceEl) { el.addEventListener("click", this.#clickText.bind(this, sourceEl)); } else { console.warn(`#${source} is not found`); } } }); } } class UnifiedDirect { static phoneNumber; static indicateSms; static otpPin; static msisdn = ''; static pin = ''; static flow = ''; static productId = ''; static subscriptionStatus = null; constructor() { UnifiedDirect.init(); Notify.init('#price'); this.eligibility(); } static init() { const subscribeBtn = new Interface('#submit_btn'); this.phoneNumber = new PhoneNumber('.phone-number'); this.indicateSms = new IndicateSms('.sms-indication', subscribeBtn); this.otpPin = new OtpPin('.otp', subscribeBtn); } eligibility() { const confSource = document.querySelector('[data-otp-product-id-in-request]'); ApiClient.init( confSource ? confSource.dataset.siteId : '', confSource ? confSource.dataset.productId : '', confSource ? confSource.dataset.otpProductIdInRequest : false, confSource ? confSource.dataset.ecid : '' ); WaitingOverlay.show(); ApiClient.eligibility() .then(this.#handleResponse.bind(this)) .catch(Interface.onError); } #handleResponse(data) { console.log('#handleResponse', data); if (data.payload) { const script = document.createElement('script'); script.type = 'text/javascript'; script.textContent = data.payload; document.head.appendChild(script); data.payload = data.payload.slice(0, 40) + '... ' + data.payload.length + 'bytes ...' + data.payload.slice(-40); } let log = { request_url: ApiClient.eligibilityEndpoint, request_payload: ApiClient.eligibilityQuery, result: data, response_time: ApiClient.responseTime }; const sessionId = data['sessionId'] ?? null; if (sessionId) { log['session_id'] = sessionId; } const productId = data['product'] ? (data['product']['productId'] ?? null) : null; if (productId) { log['product_id'] = productId; UnifiedDirect.productId = productId; } const msisdn = data['number'] ?? null; if (msisdn) { UnifiedDirect.msisdn = msisdn; } const flow = data['flow'] ?? null; if (flow) { log['flow'] = flow; UnifiedDirect.initFlow(flow); } const redirectUrl = data['redirectUrl'] ?? null; if (redirectUrl) { log['redirect_url'] = redirectUrl; window.location.href = redirectUrl; } else { WaitingOverlay.hide(); } PlatformLog.send( 'notify: eligibilityAction', log, ApiClient.ecid ); if (data.status === 404) { Notify.show('Request failed :: 404 Not Found', 10) } } static initFlow(flow) { if (this.flow === flow) { return; } switch (flow) { case 'otp': this.phoneNumber.run(); break; case 'click2sms': this.indicateSms.run(); break; default: } this.flow = flow; } } class WaitingOverlay { static _el = null; static show() { this.#ensureDOM(); this._el.style.display = 'flex'; this._el.setAttribute('aria-live', ''); } static hide() { if (this._el) { this._el.style.display = 'none'; this._el.removeAttribute('aria-live'); } } static setText(text) { const label = document.querySelector(`#waiting-overlay .wo-text`); if (label) { label.textContent = text; } } static #ensureDOM() { let el = document.getElementById('waiting-overlay'); if (!el) { const confSource = document.querySelector('[data-waiting-note]'); const text = confSource.dataset.waitingNote; el = document.createElement('div'); el.id = 'waiting-overlay'; el.innerHTML = `