(function () { var INVITE_CODE_KEYS = [ 'invite_code', 'inviteCode', 'code', 'invitationCode', ]; var SHARE_NAME_KEYS = ['name', 'nickname', 'inviterName', 'userName']; var SHARE_AVATAR_KEYS = ['avatar', 'avatarUrl', 'avatar_url', 'headUrl']; var PASS_THROUGH_KEYS = [ 'env', 'app_code', 'appCode', 'lang', 'downloadUrl', 'download_url', 'androidUrl', 'android_url', 'iosUrl', 'ios_url', 'url', ]; var params = new URLSearchParams(window.location.search); var state = { inviteCode: firstParam(INVITE_CODE_KEYS), inviterName: firstParam(SHARE_NAME_KEYS), inviterAvatar: firstParam(SHARE_AVATAR_KEYS), inviteLink: '', inviteRewardStatus: null, rankRows: [], rankLoaded: false, rankLoading: false, claiming: {}, }; function $(selector) { return document.querySelector(selector); } function firstParam(keys) { for (var i = 0; i < keys.length; i += 1) { var value = String(params.get(keys[i]) || '').trim(); if (value) return value; } return ''; } function textValue(value) { return String(value || '').trim(); } function readPath(source, path) { var value = source; for (var i = 0; i < path.length; i += 1) { if (!value || typeof value !== 'object') return ''; value = value[path[i]]; } return textValue(value); } function firstPath(source, paths) { for (var i = 0; i < paths.length; i += 1) { var value = readPath(source, paths[i]); if (value) return value; } return ''; } function currentLandingURL() { return new URL('../invite-landing/', window.location.href); } function setInviteCodeParams(searchParams, inviteCode) { searchParams.set('invite_code', inviteCode); searchParams.set('inviteCode', inviteCode); searchParams.set('code', inviteCode); } function buildInviteLink() { var code = textValue(state.inviteCode); if (!code) return ''; var url = currentLandingURL(); PASS_THROUGH_KEYS.forEach(function (key) { var value = textValue(params.get(key)); if (value) url.searchParams.set(key, value); }); setInviteCodeParams(url.searchParams, code); if (state.inviterName) { url.searchParams.set('name', state.inviterName); url.searchParams.set('nickname', state.inviterName); } if (state.inviterAvatar) { url.searchParams.set('avatar', state.inviterAvatar); url.searchParams.set('avatar_url', state.inviterAvatar); } return url.toString(); } function renderInviteShare() { state.inviteLink = buildInviteLink(); var link = $('#inviteLink'); var code = $('#inviteCode'); if (link) link.textContent = state.inviteLink || '--'; if (code) code.textContent = state.inviteCode || '--'; } function readOverviewPayload(payload) { return payload && payload.user ? payload.user : payload; } function applyCurrentUser(payload) { var data = readOverviewPayload(payload || {}); var profile = data.profile || data; var inviteCode = firstPath(data, [ ['invite', 'my_invite_code'], ['invite', 'myInviteCode'], ['profile', 'invite', 'my_invite_code'], ['profile', 'invite', 'myInviteCode'], ['my_invite_code'], ['myInviteCode'], ['invite_code'], ['inviteCode'], ]); var name = firstPath(data, [ ['profile', 'username'], ['profile', 'nickname'], ['username'], ['nickname'], ]); var avatar = firstPath(data, [ ['profile', 'avatar'], ['profile', 'avatar_url'], ['avatar'], ['avatar_url'], ]); if (inviteCode) state.inviteCode = inviteCode; if (name) state.inviterName = name; if (avatar) state.inviterAvatar = avatar; var inviteOverview = data.invite || profile.invite || {}; var inviteCount = inviteOverview.invite_count; var validInviteCount = inviteOverview.valid_invite_count; if ( inviteCount !== undefined && inviteCount !== null && $('#statInvitees') ) { $('#statInvitees').textContent = inviteCount; } if ( validInviteCount !== undefined && validInviteCount !== null && $('#statEligible') ) { $('#statEligible').textContent = validInviteCount; } renderInviteShare(); } function loadCurrentUser() { var api = window.HyAppAPI || {}; var userAPI = api.user || api.users; var hasToken = api.getAccessToken && api.getAccessToken(); var userPromise = Promise.resolve(); if (hasToken && api.get) { userPromise = window.HyAppAPI.get( '/api/v1/users/me/invite-overview' ) .then(applyCurrentUser) .catch(function () {}); } if (window.HyAppParams && window.HyAppParams.loadUser) { return userPromise .then(function () { return window.HyAppParams.loadUser(); }) .then(function (current) { if (current && current.user) applyCurrentUser(current.user); }); } if (userAPI) { if (userAPI.overview) { return userPromise .then(function () { return userAPI.overview(); }) .then(applyCurrentUser); } if (userAPI.me) { return userPromise .then(function () { return userAPI.me(); }) .then(applyCurrentUser); } } return userPromise; } function toast(message) { if (window.HyToast && window.HyToast.show) { window.HyToast.show(message); return; } window.alert(message); } function t(key, fallback) { if (window.HyAppI18n && window.HyAppI18n.t) { return window.HyAppI18n.t(key, fallback); } return fallback || key; } function formatPeople(value) { return t('invite.peopleSuffix', '{value} people').replace( '{value}', value ); } function formatNumber(value) { return new Intl.NumberFormat( window.HyAppI18n && window.HyAppI18n.lang ? window.HyAppI18n.lang() : 'en' ).format(Number(value || 0)); } function numberValue(value) { var number = Number(value || 0); return Number.isFinite(number) ? number : 0; } function field(source, snakeKey, camelKey) { if (!source || typeof source !== 'object') return undefined; if (Object.prototype.hasOwnProperty.call(source, snakeKey)) { return source[snakeKey]; } if ( camelKey && Object.prototype.hasOwnProperty.call(source, camelKey) ) { return source[camelKey]; } return undefined; } function setText(selector, value) { var node = $(selector); if (node) node.textContent = value; } function normalizeRankPayload(payload) { if (!payload || typeof payload !== 'object') return []; var items = field(payload, 'entries', 'entries'); if (!Array.isArray(items)) items = field(payload, 'items', 'items'); if (!Array.isArray(items)) return []; return items .map(function (item, index) { var user = field(item, 'user', 'user') || {}; var userID = textValue(field(item, 'user_id', 'userId')); return { rank: numberValue(field(item, 'rank_no', 'rankNo')) || index + 1, name: textValue(field(user, 'username', 'username')) || textValue( field(user, 'display_user_id', 'displayUserId') ) || (userID ? 'ID ' + userID : '--'), avatar: textValue(field(user, 'avatar', 'avatar')), score: numberValue( field(item, 'valid_invite_count', 'validInviteCount') ), }; }) .filter(function (item) { return item.rank > 0 && item.score > 0; }); } function pad(value) { return String(Math.max(0, value)).padStart(2, '0'); } function updateCountdown() { var now = Date.now(); var end = new Date(); end.setDate(end.getDate() + 1); end.setHours(0, 0, 0, 0); var diff = Math.max(0, end.getTime() - now); var totalSeconds = Math.floor(diff / 1000); var days = Math.floor(totalSeconds / 86400); var hours = Math.floor((totalSeconds % 86400) / 3600); var minutes = Math.floor((totalSeconds % 3600) / 60); var seconds = totalSeconds % 60; $('#days').textContent = pad(days); $('#hours').textContent = pad(hours); $('#minutes').textContent = pad(minutes); $('#seconds').textContent = pad(seconds); } function renderRank() { var list = $('#rankList'); if (!list) return; list.textContent = ''; if (state.rankLoading && !state.rankRows.length) { list.appendChild( emptyTaskNode(t('invite.rankLoading', 'Loading rank...')) ); return; } if (!state.rankRows.length) { list.appendChild( emptyTaskNode(t('invite.noRankData', 'No rank data')) ); return; } state.rankRows.forEach(function (item) { var row = document.createElement('article'); var rank = document.createElement('span'); var medal = document.createElement('img'); var avatar = document.createElement('img'); var name = document.createElement('strong'); var score = document.createElement('span'); row.className = 'rank-row'; rank.className = 'rank-no'; medal.className = 'rank-medal'; avatar.className = 'rank-avatar'; name.className = 'rank-name'; score.className = 'rank-score'; if (item.rank <= 3) { medal.src = './assets/medal-' + item.rank + '.png'; medal.alt = 'Rank ' + item.rank; rank.textContent = ''; } else { medal.alt = ''; rank.textContent = item.rank; } avatar.src = item.avatar || './assets/rank-avatar.png'; avatar.onerror = function () { avatar.onerror = null; avatar.src = './assets/rank-avatar.png'; }; avatar.alt = ''; name.textContent = item.name; score.textContent = formatPeople(formatNumber(item.score)); row.appendChild(rank); row.appendChild( item.rank <= 3 ? medal : document.createElement('span') ); row.appendChild(avatar); row.appendChild(name); row.appendChild(score); list.appendChild(row); }); } function normalizeInviteRewardStatus(payload) { if (!payload || typeof payload !== 'object') return null; return payload.status && typeof payload.status === 'object' ? payload.status : payload; } function claimKey(rewardType, tierID) { return rewardType + ':' + tierID; } function tierID(tier) { return numberValue(field(tier, 'tier_id', 'tierId')); } function tierRewardType(tier) { return String(field(tier, 'reward_type', 'rewardType') || ''); } function tierName(tier, index, rewardType) { return ( textValue(field(tier, 'tier_name', 'tierName')) || textValue(field(tier, 'tier_code', 'tierCode')) || (rewardType === 'recharge' ? t('invite.rechargeMilestone', 'Recharge milestone {index}') : t( 'invite.validInviteMilestone', 'Invitation milestone {index}' ) ).replace('{index}', index + 1) ); } function thresholdValue(tier, rewardType) { return rewardType === 'valid_invite' ? numberValue( field( tier, 'threshold_valid_invite_count', 'thresholdValidInviteCount' ) ) : numberValue( field(tier, 'threshold_coin_amount', 'thresholdCoinAmount') ); } function rewardCoinAmount(tier) { return numberValue( field(tier, 'reward_coin_amount', 'rewardCoinAmount') ); } function tierClaimed(tier) { return Boolean(field(tier, 'claimed', 'claimed')); } function tierReached(tier) { return Boolean(field(tier, 'reached', 'reached')); } function tierClaimable(tier, status) { var rewardType = tierRewardType(tier); var progress = (status && field(status, 'progress', 'progress')) || {}; if ( !Boolean(field(status || {}, 'enabled', 'enabled')) || tierClaimed(tier) ) { return false; } if (rewardType === 'recharge') { return ( tierID(tier) === numberValue( field( progress, 'recharge_claimable_tier_id', 'rechargeClaimableTierId' ) ) ); } return Boolean(field(tier, 'claimable', 'claimable')); } function taskDescription(tier, rewardType) { var threshold = formatNumber(thresholdValue(tier, rewardType)); var reward = formatNumber(rewardCoinAmount(tier)); if (rewardType === 'valid_invite') { return t( 'invite.validInviteTaskDescription', 'Invite {threshold} valid friends, reward {reward} coins' ) .replace('{threshold}', threshold) .replace('{reward}', reward); } return t( 'invite.rechargeTaskDescription', 'Invitees recharge {threshold} coins, reward {reward} coins' ) .replace('{threshold}', threshold) .replace('{reward}', reward); } function emptyTaskNode(message) { var node = document.createElement('p'); node.className = 'task-empty'; node.textContent = message; return node; } function buildClaimButton(tier, status) { var rewardType = tierRewardType(tier); var id = tierID(tier); var key = claimKey(rewardType, id); var claimed = tierClaimed(tier); var ready = tierClaimable(tier, status); var button = document.createElement('button'); button.className = 'claim-button'; if (ready) button.classList.add('is-ready'); button.type = 'button'; button.disabled = !ready || Boolean(state.claiming[key]); button.textContent = state.claiming[key] ? t('invite.claiming', 'Claiming') : claimed ? t('invite.claimed', 'Claimed') : t('invite.claim', 'Claim'); button.addEventListener('click', function () { claimInviteReward(tier); }); return button; } function renderRewardTier(container, tier, index, status) { var rewardType = tierRewardType(tier); var article = document.createElement('article'); var content = document.createElement('div'); var title = document.createElement('h3'); var desc = document.createElement('p'); article.className = rewardType === 'recharge' ? 'milestone' : 'quota-card'; article.classList.toggle('is-done', tierClaimed(tier)); article.classList.toggle( 'is-current', tierReached(tier) && !tierClaimed(tier) ); title.textContent = tierName(tier, index, rewardType); desc.textContent = taskDescription(tier, rewardType); content.appendChild(title); content.appendChild(desc); article.appendChild(content); article.appendChild(buildClaimButton(tier, status)); container.appendChild(article); } function renderTaskList(selector, status, rewardType) { var container = $(selector); if (!container) return; container.textContent = ''; var tiers = ((status && field(status, 'tiers', 'tiers')) || []) .filter(function (tier) { return ( tierRewardType(tier) === rewardType && String(field(tier, 'status', 'status') || 'active') !== 'inactive' ); }) .sort(function (left, right) { var sortLeft = numberValue( field(left, 'sort_order', 'sortOrder') ); var sortRight = numberValue( field(right, 'sort_order', 'sortOrder') ); if (sortLeft === sortRight) { return ( thresholdValue(left, rewardType) - thresholdValue(right, rewardType) ); } return sortLeft - sortRight; }); if (!tiers.length) { container.appendChild( emptyTaskNode(t('invite.noRewardTasks', 'No reward tasks')) ); return; } tiers.forEach(function (tier, index) { renderRewardTier(container, tier, index, status); }); } function grantedRewardTotal(status) { return ((status && field(status, 'claims', 'claims')) || []).reduce( function (total, claim) { if ( String(field(claim, 'status', 'status') || '') !== 'granted' ) { return total; } return ( total + numberValue( field(claim, 'reward_coin_amount', 'rewardCoinAmount') ) ); }, 0 ); } function renderInviteActivityReward(status) { state.inviteRewardStatus = normalizeInviteRewardStatus(status); var current = state.inviteRewardStatus || {}; var progress = field(current, 'progress', 'progress') || {}; if (state.inviteRewardStatus) { setText( '#statTotalRecharged', formatNumber( field( progress, 'total_recharge_coin_amount', 'totalRechargeCoinAmount' ) ) ); setText( '#statEligible', formatNumber( field(progress, 'valid_invite_count', 'validInviteCount') ) ); setText( '#statMyRewards', formatNumber(grantedRewardTotal(current)) ); } renderTaskList('#transportTimeline', current, 'recharge'); renderTaskList('#quotaList', current, 'valid_invite'); } function loadInviteActivityReward() { var api = window.HyAppAPI && window.HyAppAPI.inviteActivityReward; if (!api || !api.status || !window.HyAppAPI.getAccessToken()) { renderInviteActivityReward(null); return Promise.resolve(); } return api .status() .then(function (status) { renderInviteActivityReward(status); }) .catch(function () { renderInviteActivityReward(null); }); } function loadInviteActivityLeaderboard(force) { var api = window.HyAppAPI && window.HyAppAPI.inviteActivityReward; if ( state.rankLoading || (state.rankLoaded && !force) || !api || !api.leaderboard || !window.HyAppAPI.getAccessToken() ) { renderRank(); return Promise.resolve(); } state.rankLoading = true; renderRank(); return api .leaderboard({ page: 1, page_size: 50 }) .then(function (payload) { state.rankRows = normalizeRankPayload(payload); state.rankLoaded = true; state.rankLoading = false; renderRank(); }) .catch(function () { state.rankRows = []; state.rankLoaded = true; state.rankLoading = false; renderRank(); }); } function claimInviteReward(tier) { var api = window.HyAppAPI && window.HyAppAPI.inviteActivityReward; var rewardType = tierRewardType(tier); var id = tierID(tier); var key = claimKey(rewardType, id); if (!api || !api.claim || state.claiming[key]) return; if (!tierClaimable(tier, state.inviteRewardStatus)) { toast(t('invite.notAvailable', 'Not available yet')); return; } state.claiming[key] = true; renderInviteActivityReward(state.inviteRewardStatus); api.claim({ reward_type: rewardType, tier_id: id, command_id: 'invite_reward_' + rewardType + '_' + id + '_' + Date.now() + '_' + Math.random().toString(36).slice(2), }) .then(function (payload) { delete state.claiming[key]; renderInviteActivityReward( normalizeInviteRewardStatus(payload && payload.status) ); toast(t('invite.claimSuccess', 'Claimed')); }) .catch(function (error) { delete state.claiming[key]; renderInviteActivityReward(state.inviteRewardStatus); toast( (error && error.message) || t('invite.claimFailed', 'Claim failed') ); }); } function setTab(tabName) { var monthly = $('#monthlyTask'); var isQuota = tabName === 'quota'; monthly.classList.toggle('is-quota', isQuota); document.querySelectorAll('.task-tab').forEach(function (button) { var active = button.getAttribute('data-tab') === tabName; button.classList.toggle('is-active', active); button.setAttribute('aria-selected', String(active)); }); $('#transportPane').classList.toggle('is-active', !isQuota); $('#quotaPane').classList.toggle('is-active', isQuota); $('#rankPanel').hidden = !isQuota; if (isQuota) loadInviteActivityLeaderboard(false); } function copyTextFrom(selector) { var node = $(selector); var text = node ? node.textContent.trim() : ''; if (!text || text === '--') { toast(t('invite.copyFailed', 'Copy failed')); return; } function fallbackCopy() { var input = document.createElement('textarea'); input.value = text; input.setAttribute('readonly', 'readonly'); input.style.position = 'fixed'; input.style.left = '-9999px'; input.style.top = '0'; document.body.appendChild(input); input.select(); var ok = false; try { ok = document.execCommand('copy'); } catch (error) { ok = false; } document.body.removeChild(input); toast( ok ? t('invite.copied', 'Copied') : t('invite.copyFailed', 'Copy failed') ); } if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard .writeText(text) .then(function () { toast(t('invite.copied', 'Copied')); }) .catch(function () { fallbackCopy(); }); return; } fallbackCopy(); } function copyInviteLink() { copyTextFrom('#inviteLink'); } function copyInviteCode() { copyTextFrom('#inviteCode'); } function bindEvents() { document.querySelectorAll('.task-tab').forEach(function (button) { button.addEventListener('click', function () { setTab(button.getAttribute('data-tab') || 'transport'); }); }); $('#copyLinkButton').addEventListener('click', copyInviteLink); $('#copyCodeButton').addEventListener('click', copyInviteCode); document .querySelectorAll('.copy-task-button') .forEach(function (button) { button.addEventListener('click', copyInviteLink); }); } function init() { renderInviteShare(); renderRank(); updateCountdown(); setInterval(updateCountdown, 1000); bindEvents(); renderInviteActivityReward(null); Promise.all([ loadCurrentUser().catch(function () { renderInviteShare(); }), loadInviteActivityReward(), ]); } window.addEventListener('hyapp:i18n-ready', function () { renderRank(); renderInviteActivityReward(state.inviteRewardStatus); }); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();