(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 BIND_INVITE_VISIBLE_WINDOW_MS = 48 * 60 * 60 * 1000; 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: '', currentUser: null, currentUserCreatedAtMS: 0, currentUserTimingLoaded: false, serverTimeMS: 0, pendingInviteCode: '', searchedInviter: null, boundInviter: null, boundInviterLoaded: false, boundInviterLoading: null, inviteRewardStatus: null, rankRows: [], rankLoaded: false, rankLoading: false, searchingInviter: false, bindingInviter: false, viewingBoundInviter: 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 normalizeInviteCodeInput(value) { return textValue(value).toUpperCase(); } function readRawPath(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 value; } function readPath(source, path) { var value = readRawPath(source, path); 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 normalizeTimestampMS(value) { if (value === undefined || value === null || value === '') return 0; if (typeof value === 'string' && !String(value).trim()) return 0; var number = Number(value); if (Number.isFinite(number) && number > 0) { return number < 100000000000 ? number * 1000 : number; } var parsed = Date.parse(value); return Number.isFinite(parsed) && parsed > 0 ? parsed : 0; } function firstTimestampMS(source, paths) { for (var i = 0; i < paths.length; i += 1) { var value = normalizeTimestampMS(readRawPath(source, paths[i])); if (value) return value; } return 0; } function currentUserPayload(data, profile) { var target = profile || data || {}; return Boolean( target.user_id || target.userId || target.display_user_id || target.displayUserId || target.username || target.nickname || target.created_at_ms !== undefined || target.createdAtMs !== undefined || target.profile_completed !== undefined || target.profileCompleted !== undefined ); } function mergeCurrentUser(profile) { if (!profile || typeof profile !== 'object') return; state.currentUser = Object.assign({}, state.currentUser || {}, profile); } 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 applyCurrentUserTiming(data, profile) { var serverTimeMS = firstTimestampMS(data, [ ['server_time_ms'], ['serverTimeMS'], ['serverTimeMs'], ['server_time'], ['serverTime'], ]); var createdAtMS = firstTimestampMS(data, [ ['profile', 'created_at_ms'], ['profile', 'createdAtMs'], ['profile', 'created_at'], ['profile', 'createdAt'], ['profile', 'registered_at_ms'], ['profile', 'registeredAtMs'], ['profile', 'registered_at'], ['profile', 'registeredAt'], ['created_at_ms'], ['createdAtMs'], ['created_at'], ['createdAt'], ['registered_at_ms'], ['registeredAtMs'], ['registered_at'], ['registeredAt'], ]); if (!createdAtMS) { createdAtMS = firstTimestampMS(profile, [ ['created_at_ms'], ['createdAtMs'], ['created_at'], ['createdAt'], ['registered_at_ms'], ['registeredAtMs'], ['registered_at'], ['registeredAt'], ]); } if (serverTimeMS) state.serverTimeMS = serverTimeMS; if (createdAtMS) state.currentUserCreatedAtMS = createdAtMS; } function bindInviteReferenceTimeMS() { return state.serverTimeMS || Date.now(); } function canShowBindInviteButton() { var api = window.HyAppAPI || {}; var hasToken = api.getAccessToken && api.getAccessToken(); if (state.boundInviter) return true; if (hasToken && !state.currentUserTimingLoaded) return false; var current = state.currentUser || {}; var createdAtMS = state.currentUserCreatedAtMS || firstTimestampMS(current, [ ['created_at_ms'], ['createdAtMs'], ['created_at'], ['createdAt'], ['registered_at_ms'], ['registeredAtMs'], ['registered_at'], ['registeredAt'], ]); if (!createdAtMS) return true; // 48 小时窗口必须用服务端返回的当前时间优先判断;接口缺少时间时才退回本机时间,避免用户手动改系统时间影响显隐。 return ( bindInviteReferenceTimeMS() - createdAtMS <= BIND_INVITE_VISIBLE_WINDOW_MS ); } function renderBindInviteVisibility() { var button = $('#bindInviteButton'); if (!button) return; var visible = canShowBindInviteButton(); button.hidden = !visible; button.disabled = !visible; if (visible || !$('#bindInviteModal') || $('#bindInviteModal').hidden) { return; } closeBindModal(); } function applyCurrentUser(payload) { var data = readOverviewPayload(payload || {}); var profile = data.profile || data; applyCurrentUserTiming(data, profile); if (currentUserPayload(data, profile)) { state.currentUserTimingLoaded = true; mergeCurrentUser(profile); } 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(); renderBindInviteVisibility(); } function applyBoundInviteReferrer(payload) { var invite = payload && (payload.invite || payload.Invite); var inviter = normalizeInviter(payload, ''); if (!inviter) { state.boundInviter = null; renderBindInviteVisibility(); return; } inviter.inviteCode = textValue( firstObjectValue(invite, [ 'invite_code', 'inviteCode', 'my_invite_code', 'myInviteCode', ]) ) || inviter.inviteCode; state.boundInviter = inviter; state.searchedInviter = inviter; renderBindInviteVisibility(); } function loadBoundInviteReferrer() { var api = window.HyAppAPI && window.HyAppAPI.user; var rootAPI = window.HyAppAPI || {}; if (state.boundInviterLoading) return state.boundInviterLoading; if ( !api || !api.getInviteReferrer || !rootAPI.getAccessToken || !rootAPI.getAccessToken() ) { state.boundInviterLoaded = true; return Promise.resolve(); } // 已绑定关系必须每次从服务端读取;本地只缓存展示结果,避免刷新后重新进入搜索绑定流程。 state.boundInviterLoading = api .getInviteReferrer() .then(applyBoundInviteReferrer) .catch(function () {}) .then(function () { state.boundInviterLoaded = true; state.boundInviterLoading = null; }); return state.boundInviterLoading; } 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 () {}); } function loadMeProfile() { if (!hasToken || !userAPI || !userAPI.me) return Promise.resolve(); return userAPI .me() .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); }) .then(loadMeProfile); } if (userAPI) { if (userAPI.overview) { return userPromise .then(function () { return userAPI.overview(); }) .then(applyCurrentUser) .then(loadMeProfile); } if (userAPI.me) { return userPromise.then(loadMeProfile); } } return userPromise.then(loadMeProfile); } 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') || {}; var perInviteReward = numberValue( field( current, 'per_invite_inviter_reward_coin_amount', 'perInviteInviterRewardCoinAmount' ) ); if (perInviteReward > 0) { setText( '#rewardInviteAmount', '+' + formatNumber(perInviteReward) + '(Max40,000)' ); } 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 firstObjectValue(source, keys) { source = source || {}; for (var i = 0; i < keys.length; i += 1) { var value = source[keys[i]]; if (value !== undefined && value !== null && value !== '') { return value; } } return ''; } function showBoundInviterModal() { if (state.boundInviter) { state.pendingInviteCode = ''; state.searchedInviter = state.boundInviter; state.searchingInviter = false; state.bindingInviter = false; showBindResult(state.boundInviter, { bound: true }); $('#bindInviteModal').hidden = false; } } function showBindSearchModal() { state.pendingInviteCode = ''; state.searchedInviter = null; state.searchingInviter = false; state.bindingInviter = false; var input = $('#bindInviteInput'); if (input) input.value = ''; showBindInput(); $('#bindInviteModal').hidden = false; window.setTimeout(function () { if (input) input.focus(); }, 0); } function openBindModal() { if (!canShowBindInviteButton()) { renderBindInviteVisibility(); return; } var api = window.HyAppAPI || {}; if (!api.getAccessToken || !api.getAccessToken()) { toast(t('invite.loginRequired', 'Please log in first')); return; } if (state.boundInviter) { showBoundInviterModal(); return; } if (!state.boundInviterLoaded) { loadBoundInviteReferrer().then(function () { if (state.boundInviter) { showBoundInviterModal(); return; } showBindSearchModal(); }); return; } showBindSearchModal(); } function closeBindModal() { $('#bindInviteModal').hidden = true; state.searchingInviter = false; state.bindingInviter = false; state.viewingBoundInviter = false; renderBindActions(); } function showBindInput() { $('#bindInputCard').hidden = false; $('#bindResultCard').hidden = true; state.searchedInviter = null; state.viewingBoundInviter = false; renderBindActions(); } function showBindResult(inviter, options) { var avatar = $('#inviterAvatar'); var name = $('#inviterName'); var displayID = $('#inviterDisplayId'); if (avatar) { avatar.src = inviter.avatar || './assets/rank-avatar.png'; avatar.onerror = function () { avatar.onerror = null; avatar.src = './assets/rank-avatar.png'; }; } if (name) name.textContent = inviter.name || '--'; if (displayID) displayID.textContent = inviter.displayUserID || '--'; $('#bindInputCard').hidden = true; $('#bindResultCard').hidden = false; state.viewingBoundInviter = Boolean(options && options.bound); renderBindActions(); } function renderBindActions() { var cancel = $('#bindCancelButton'); var confirm = $('#confirmBindButton'); var input = $('#bindInviteInput'); var busy = state.searchingInviter || state.bindingInviter; if (confirm) { confirm.hidden = state.viewingBoundInviter; confirm.disabled = state.viewingBoundInviter || busy; if (!state.viewingBoundInviter) { confirm.textContent = state.bindingInviter ? t('invite.binding', 'Binding...') : state.searchingInviter ? t('invite.searchingInviter', 'Searching...') : t('invite.confirm', 'Confirm'); } } if (cancel) { cancel.disabled = busy; cancel.textContent = state.viewingBoundInviter ? t('invite.close', 'Close') : t('invite.cancel', 'Cancel'); } if (input) input.disabled = busy; } function normalizeInviter(payload, fallbackDisplayUserID) { payload = payload || {}; var wrapper = payload.inviter || payload.Inviter || payload; var identity = payload.identity || payload.Identity || payload; var profile = payload.profile || payload.Profile || wrapper.profile || wrapper.Profile || wrapper; var userID = textValue(firstObjectValue(profile, ['user_id', 'userId'])) || textValue(firstObjectValue(identity, ['user_id', 'userId'])); var displayUserID = textValue( firstObjectValue(profile, [ 'pretty_display_user_id', 'prettyDisplayUserId', 'display_user_id', 'displayUserId', ]) ) || textValue( firstObjectValue(identity, [ 'pretty_display_user_id', 'prettyDisplayUserId', 'display_user_id', 'displayUserId', ]) ) || fallbackDisplayUserID; var name = textValue( firstObjectValue(profile, ['username', 'nickname', 'name']) ) || displayUserID; var avatar = textValue( firstObjectValue(profile, ['avatar', 'avatar_url', 'avatarUrl']) ); if (!userID && !displayUserID) return null; return { userID: userID, displayUserID: displayUserID, name: name, avatar: avatar, }; } function isCurrentUser(inviter) { var current = state.currentUser || {}; var currentUserID = textValue( firstObjectValue(current, ['user_id', 'userId']) ); var currentDisplayID = textValue( firstObjectValue(current, [ 'pretty_display_user_id', 'prettyDisplayUserId', 'display_user_id', 'displayUserId', ]) ); return Boolean( (currentUserID && currentUserID === inviter.userID) || (currentDisplayID && currentDisplayID === inviter.displayUserID) ); } function searchInviter() { if (state.searchingInviter) return; var input = $('#bindInviteInput'); var inviteCode = normalizeInviteCodeInput(input && input.value); var api = window.HyAppAPI && window.HyAppAPI.user; if (input && input.value !== inviteCode) input.value = inviteCode; if (!inviteCode) { toast( t('invite.searchRequired', 'Please enter an invitation code') ); return; } if (!api || !api.searchInviteReferrer) { toast(t('invite.bindFailed', 'Bind failed')); return; } state.searchingInviter = true; state.pendingInviteCode = inviteCode; renderBindActions(); // 搜索入口只提交邀请码,由 server 解析归属并按当前用户国家过滤,前端不再用可被篡改的用户 ID 作为绑定入参。 api.searchInviteReferrer(inviteCode) .then(function (payload) { var inviter = normalizeInviter(payload, ''); state.searchingInviter = false; if (!inviter) { renderBindActions(); toast(t('invite.inviterNotFound', 'Inviter not found')); return; } if (isCurrentUser(inviter)) { renderBindActions(); toast( t( 'invite.cannotBindSelf', 'You cannot bind yourself as inviter' ) ); return; } inviter.inviteCode = inviteCode; state.searchedInviter = inviter; showBindResult(inviter); }) .catch(function (error) { state.searchingInviter = false; renderBindActions(); toast( (error && error.message) || t('invite.inviterNotFound', 'Inviter not found') ); }); } function confirmBindInviter() { if (state.viewingBoundInviter) { closeBindModal(); return; } if (state.bindingInviter) return; var api = window.HyAppAPI && window.HyAppAPI.user; var inviter = state.searchedInviter; if (!inviter) { searchInviter(); return; } if (!api || !api.bindInviteReferrer) { toast(t('invite.bindFailed', 'Bind failed')); return; } state.bindingInviter = true; renderBindActions(); api.bindInviteReferrer(inviter.inviteCode || state.pendingInviteCode) .then(function (response) { var invite = response && response.invite; state.bindingInviter = false; renderBindActions(); if (invite && (invite.bound || invite.invite_code)) { var boundInviter = normalizeInviter(response, '') || inviter; boundInviter.inviteCode = textValue( firstObjectValue(invite, [ 'invite_code', 'inviteCode', ]) ) || inviter.inviteCode || state.pendingInviteCode; state.boundInviter = boundInviter; state.boundInviterLoaded = true; state.searchedInviter = boundInviter; renderBindInviteVisibility(); toast(t('invite.bindSuccess', 'Bound successfully')); closeBindModal(); loadCurrentUser().catch(function () {}); loadBoundInviteReferrer(); loadInviteActivityReward(); return; } toast( t( 'invite.bindNoChange', 'Invite referrer is already bound.' ) ); }) .catch(function (error) { state.bindingInviter = false; renderBindActions(); toast( (error && error.message) || t('invite.bindFailed', 'Bind failed') ); }); } 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); $('#bindInviteButton').addEventListener('click', openBindModal); $('#bindCancelButton').addEventListener('click', closeBindModal); $('#confirmBindButton').addEventListener('click', confirmBindInviter); $('#bindInviteModal').addEventListener('click', function (event) { if (event.target === $('#bindInviteModal')) closeBindModal(); }); $('#bindInviteInput').addEventListener('keydown', function (event) { if (event.key === 'Enter') confirmBindInviter(); }); $('#bindInviteInput').addEventListener('input', function (event) { var nextValue = normalizeInviteCodeInput(event.target.value); if (event.target.value !== nextValue) event.target.value = nextValue; }); document .querySelectorAll('.copy-task-button') .forEach(function (button) { button.addEventListener('click', copyInviteLink); }); } function init() { renderInviteShare(); renderRank(); updateCountdown(); setInterval(updateCountdown, 1000); renderBindInviteVisibility(); bindEvents(); renderInviteActivityReward(null); Promise.all([ loadCurrentUser().catch(function () { renderInviteShare(); }), loadBoundInviteReferrer(), loadInviteActivityReward(), ]); } window.addEventListener('hyapp:i18n-ready', function () { renderRank(); renderBindActions(); renderInviteActivityReward(state.inviteRewardStatus); }); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();