2026-07-01 14:37:24 +08:00

1349 lines
45 KiB
JavaScript

(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();
}
})();