1349 lines
45 KiB
JavaScript
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();
|
|
}
|
|
})();
|