const query = new URLSearchParams(window.location.search); const LOCAL_INVITE_API_BASE = "http://127.0.0.1:2900/"; const LOCAL_PROXY_API_BASE = "/api/"; const LOCAL_FILE_PROXY_API_BASE = "http://127.0.0.1:8800/api/"; const ONLINE_INVITE_API_BASE = "https://jvapi.haiyihy.com/"; const ONLINE_INVITE_API_PREFIX = "/go"; const DEFAULT_AVATAR = "./assets/avatar-sample.png"; const INVITEE_DISPLAY_GIFT_REWARD = 1000; const DEFAULT_RESET_PERIOD_MS = 30 * 86400 * 1000; const DEFAULT_REQUEST_HEADERS = { "Req-Sys-Origin": "origin=LIKEI;originChild=LIKEI", "Req-Client": "Android", "Req-App-Intel": "version=1.0.0;build=1;channel=Web;model=H5;sysVersion=Web", "Req-Lang": "en", "Req-Version": "V2", "Req-Zone": "Asia/Shanghai", }; const DEFAULT_TASK_NOTES = { transport: "(Only the highest-level bonuses may be awarded)", quota: "(Only invited users who reach the required recharge amount count as valid invites)", }; const INVITE_SHARE_COPY_PREFIX = "Download the app, invite your friends to join, and earn 100,000 coins plus a special welcome gift!"; const DEFAULTS = { view: "invite", inviteLink: "", inviteCode: "", invitees: "0", eligibleVoters: "0", rechargeTotal: "0", myRewards: "0", friendName: "Name", downloadUrl: "", inviterBindReward: 0, rechargeReward: 0, giftReward: INVITEE_DISPLAY_GIFT_REWARD, }; const FALLBACK_TASKS = { transport: [], quota: [], }; const state = { view: resolveInitialView(), apiBase: resolveAPIBase(), apiPrefix: resolveAPIPrefix(), authorization: resolveAuthorization(), requestHeaders: resolveRequestHeaders(), landingCode: resolveLandingCode(), inviteLink: query.get("inviteLink") || DEFAULTS.inviteLink, inviteCode: query.get("inviteCode") || DEFAULTS.inviteCode, invitees: query.get("invitees") || DEFAULTS.invitees, eligibleVoters: query.get("eligible") || DEFAULTS.eligibleVoters, rechargeTotal: query.get("recharge") || DEFAULTS.rechargeTotal, myRewards: query.get("rewards") || DEFAULTS.myRewards, friendName: query.get("friendName") || DEFAULTS.friendName, inviterAvatar: DEFAULT_AVATAR, downloadUrl: query.get("downloadUrl") || DEFAULTS.downloadUrl, inviterBindReward: query.get("inviterReward") || DEFAULTS.inviterBindReward, rechargeReward: query.get("rechargeReward") || DEFAULTS.rechargeReward, giftReward: INVITEE_DISPLAY_GIFT_REWARD, taskSet: normalizeTaskSet(query.get("taskSet") || "quota"), tasks: { transport: cloneTasks(FALLBACK_TASKS.transport), quota: cloneTasks(FALLBACK_TASKS.quota), }, rankItems: [], serverOffsetMs: 0, countdownEndAtMs: Date.now() + DEFAULT_RESET_PERIOD_MS, countdownTimer: null, pendingClaimRuleId: "", }; const els = { views: [...document.querySelectorAll("[data-view]")], inviteLinkText: document.getElementById("inviteLinkText"), inviteCodeText: document.getElementById("inviteCodeText"), statInvitees: document.getElementById("statInvitees"), statEligible: document.getElementById("statEligible"), statRecharge: document.getElementById("statRecharge"), statRewards: document.getElementById("statRewards"), taskList: document.getElementById("taskList"), taskNote: document.getElementById("taskNote"), taskTabs: [...document.querySelectorAll("[data-task-set]")], navButtons: [...document.querySelectorAll("[data-nav-view]")], copyButtons: [...document.querySelectorAll("[data-copy-target]")], rankList: document.getElementById("rankList"), rankLists: [...document.querySelectorAll("[data-rank-list]")], giftTitle: document.getElementById("giftTitle"), giftAvatarImage: document.getElementById("giftAvatarImage"), downloadButton: document.getElementById("downloadButton"), inviteRewardAmounts: [ ...document.querySelectorAll('.reward-panel .reward-amount'), ], giftRewardAmount: document.querySelector('.gift-panel .reward-amount'), toast: document.getElementById("pageToast"), countdown: { days: document.querySelector('[data-countdown="days"]'), hours: document.querySelector('[data-countdown="hours"]'), minutes: document.querySelector('[data-countdown="minutes"]'), seconds: document.querySelector('[data-countdown="seconds"]'), }, }; function normalizeView(value) { if (value === "rank" || value === "gift") { return value; } return "invite"; } function normalizeTaskSet(value) { return value === "transport" ? "transport" : "quota"; } function trimValue(value) { return value == null ? "" : String(value).trim(); } function resolveLandingCode() { return trimValue( query.get("code") || query.get("landingCode") || query.get("invitationCode") || query.get("inviteCode"), ); } function hasAuthorizationInput() { return Boolean( trimValue(query.get("authorization")) || trimValue(query.get("Authorization")) || trimValue(query.get("token")) || trimValue(query.get("token?")) || trimValue(query.get("accessToken")) || trimValue(query.get("auth")) || (typeof window.__APP_INVITE_AUTHORIZATION__ === "string" && trimValue(window.__APP_INVITE_AUTHORIZATION__)) || (typeof window.__APP_INVITE_TOKEN__ === "string" && trimValue(window.__APP_INVITE_TOKEN__)), ); } function resolveInitialView() { const explicitView = trimValue(query.get("view")); if (explicitView) { return normalizeView(explicitView); } if (resolveLandingCode() && !hasAuthorizationInput()) { return "gift"; } return DEFAULTS.view; } function normalizeBaseURL(base) { const value = trimValue(base); if (!value) { return resolveDefaultAPIBase(); } return value.endsWith("/") ? value : `${value}/`; } function isLocalHost(hostname = window.location.hostname) { return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1"; } function isOnlineAPIBase(base) { try { return new URL(base, window.location.href).origin === new URL(ONLINE_INVITE_API_BASE).origin; } catch { return false; } } function isAbsoluteURL(value) { return /^https?:\/\//i.test(trimValue(value)); } function resolveDefaultAPIBase() { if (window.location.protocol === "file:") { return LOCAL_FILE_PROXY_API_BASE; } if (window.location.protocol === "http:" && isLocalHost()) { return LOCAL_PROXY_API_BASE; } if (window.location.protocol === "https:" && !isLocalHost()) { return ONLINE_INVITE_API_BASE; } return LOCAL_INVITE_API_BASE; } function resolveAPIBase() { const queryBase = trimValue(query.get("apiBase")); if (queryBase) { return normalizeBaseURL(queryBase); } if (typeof window.__APP_INVITE_API_BASE__ === "string") { return normalizeBaseURL(window.__APP_INVITE_API_BASE__); } return normalizeBaseURL(resolveDefaultAPIBase()); } function resolveAPIPrefix() { const queryPrefix = trimValue(query.get("apiPrefix")); if (queryPrefix) { return queryPrefix; } if (typeof window.__APP_INVITE_API_PREFIX__ === "string") { return trimValue(window.__APP_INVITE_API_PREFIX__); } return isOnlineAPIBase(resolveAPIBase()) ? ONLINE_INVITE_API_PREFIX : ""; } function resolveAuthorization() { const candidates = [ query.get("authorization"), query.get("Authorization"), query.get("token"), query.get("token?"), query.get("accessToken"), query.get("auth"), typeof window.__APP_INVITE_AUTHORIZATION__ === "string" ? window.__APP_INVITE_AUTHORIZATION__ : "", typeof window.__APP_INVITE_TOKEN__ === "string" ? window.__APP_INVITE_TOKEN__ : "", ]; const value = candidates.find((item) => trimValue(item)); if (!value) { return ""; } const normalized = trimValue(value); if (/^bearer\s+/i.test(normalized)) { return normalized; } return `Bearer ${normalized}`; } function resolveRequestHeaders() { const headers = { ...DEFAULT_REQUEST_HEADERS }; const sysOrigin = trimValue(query.get("sysOrigin") || query.get("origin")); const sysOriginChild = trimValue( query.get("sysOriginChild") || query.get("originChild") || sysOrigin, ); const reqClient = trimValue(query.get("reqClient") || query.get("client")); const reqLang = trimValue(query.get("reqLang") || query.get("lang")); const reqZone = trimValue(query.get("reqZone") || query.get("zone")); const reqVersion = trimValue(query.get("reqVersion") || query.get("version")); const reqImei = trimValue(query.get("reqImei") || query.get("imei")); const appVersion = trimValue(query.get("appVersion")) || "1.0.0"; const buildVersion = trimValue(query.get("buildVersion") || query.get("build")) || "1"; const appChannel = trimValue(query.get("appChannel") || query.get("channel")) || "Web"; if (sysOrigin) { headers["Req-Sys-Origin"] = `origin=${sysOrigin};originChild=${sysOriginChild || sysOrigin}`; } if (reqClient) { headers["Req-Client"] = reqClient; } if (reqLang) { headers["Req-Lang"] = reqLang; } if (reqZone) { headers["Req-Zone"] = reqZone; } if (reqVersion) { headers["Req-Version"] = reqVersion; } if (reqImei) { headers["Req-Imei"] = reqImei; } headers["Req-App-Intel"] = buildAppIntelHeader({ version: appVersion, build: buildVersion, channel: appChannel, model: trimValue(query.get("model")) || "H5", sysVersion: trimValue(query.get("sysVersion")) || "Web", imei: reqImei, }); return headers; } function buildAppIntelHeader(parts) { const items = []; const version = trimValue(parts?.version) || "1.0.0"; const build = trimValue(parts?.build) || "1"; const channel = trimValue(parts?.channel) || "Web"; const model = trimValue(parts?.model); const sysVersion = trimValue(parts?.sysVersion); const imei = trimValue(parts?.imei); items.push(`version=${version}`); items.push(`build=${build}`); items.push(`channel=${channel}`); if (model) { items.push(`model=${model}`); } if (sysVersion) { items.push(`sysVersion=${sysVersion}`); } if (imei) { items.push(`Req-Imei=${imei}`); } return items.join(";"); } function splitHeaderPairs(value) { const result = {}; trimValue(value) .split(";") .forEach((item) => { const index = item.indexOf("="); if (index > -1) { result[item.slice(0, index)] = item.slice(index + 1); } }); return result; } function parseAccessOrigin(raw) { const data = typeof raw === "string" ? JSON.parse(raw) : raw; if (!data || typeof data !== "object") { return {}; } const appIntel = splitHeaderPairs(data["Req-App-Intel"] || data["req-app-intel"]); const sysOrigin = splitHeaderPairs(data["Req-Sys-Origin"] || data["req-sys-origin"]); return { authorization: data.Authorization || data.authorization, reqLang: data["Req-Lang"] || data["req-lang"], reqClient: data["Req-Client"] || data["req-client"], reqImei: data["Req-Imei"] || data["req-imei"] || appIntel["Req-Imei"], appVersion: appIntel.version, buildVersion: appIntel.build, appChannel: appIntel.channel, model: appIntel.model, sysVersion: appIntel.sysVersion, sysOrigin: sysOrigin.origin, sysOriginChild: sysOrigin.originChild || sysOrigin.child, }; } function setHeadersFromApp(info) { if (!info || typeof info !== "object") { return; } const next = {}; const authorization = normalizeAuthorizationValue(info.authorization); if (authorization) { state.authorization = authorization; } if (info.reqLang) { next["Req-Lang"] = info.reqLang; } if (info.reqClient) { next["Req-Client"] = info.reqClient; } if (info.reqImei) { next["Req-Imei"] = info.reqImei; } if (info.sysOrigin) { next["Req-Sys-Origin"] = `origin=${info.sysOrigin};originChild=${info.sysOriginChild || info.sysOrigin}`; } if (info.appVersion || info.buildVersion || info.appChannel || info.model || info.sysVersion) { next["Req-App-Intel"] = buildAppIntelHeader({ version: info.appVersion, build: info.buildVersion, channel: info.appChannel, model: info.model, sysVersion: info.sysVersion, imei: info.reqImei, }); } state.requestHeaders = { ...state.requestHeaders, ...next }; } function normalizeAuthorizationValue(value) { const normalized = trimValue(value); if (!normalized) { return ""; } if (/^bearer\s+/i.test(normalized)) { return normalized; } return `Bearer ${normalized}`; } function connectToApp() { return new Promise((resolve) => { let finished = false; const finish = (value) => { if (finished) { return; } finished = true; resolve(value); }; window.renderData = finish; window.getIosAccessOriginParam = finish; if (!(window.app || window.webkit || window.FlutterPageControl || window.FlutterApp)) { finish(null); return; } let attempt = 0; const poll = () => { attempt += 1; if (window.app && typeof window.app.getAccessOrigin === "function") { try { finish(window.app.getAccessOrigin()); } catch { finish(null); } return; } if (attempt < 50) { window.setTimeout(poll, 100); return; } if (window.FlutterApp && typeof window.FlutterApp.postMessage === "function") { window.FlutterApp.postMessage("requestAccessOrigin"); } window.setTimeout(() => finish(null), 300); }; poll(); }).then((value) => { if (value) { try { setHeadersFromApp(parseAccessOrigin(value)); } catch { // App bridge data is best-effort; query token and default gateway headers still work. } } }); } function escapeHtml(value) { return String(value).replace(/[&<>"']/g, (match) => { const entityMap = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }; return entityMap[match] || match; }); } function cloneTasks(list) { return list.map((item) => ({ ...item })); } function showToast(text) { if (!text) { return; } els.toast.textContent = text; els.toast.classList.add("show"); window.clearTimeout(showToast.timer); showToast.timer = window.setTimeout(() => { els.toast.classList.remove("show"); }, 1800); } function loadImage(image) { if (!image || image.getAttribute("src")) { return; } const src = image.getAttribute("data-src"); if (src) { image.setAttribute("src", src); } } function setImageSource(image, src, fallbackSrc = "") { if (!(image instanceof HTMLImageElement)) { return; } const nextSrc = trimValue(src) || fallbackSrc; if (!nextSrc) { return; } image.setAttribute("src", nextSrc); image.setAttribute("data-src", nextSrc); } function hydrateViewAssets(view) { const targets = { invite: [ document.getElementById("inviteHeroImage"), ...document.querySelectorAll('.view[data-view="invite"] .lazy-image'), ], rank: [ ...document.querySelectorAll('.view[data-view="rank"] [data-badge-src]'), ...document.querySelectorAll('.view[data-view="rank"] [data-avatar-src]'), ], gift: [ document.getElementById("giftBgImage"), document.getElementById("giftAvatarImage"), document.getElementById("phoneHomeImage"), document.getElementById("phoneGiftImage"), ...document.querySelectorAll('.view[data-view="gift"] .lazy-image'), ], }; (targets[view] || []).forEach((item) => { if (item instanceof HTMLImageElement) { loadImage(item); } }); } function setView(view) { state.view = normalizeView(view); els.views.forEach((section) => { section.classList.toggle("active", section.getAttribute("data-view") === state.view); }); hydrateViewAssets(state.view); const url = new URL(window.location.href); if (state.view === DEFAULTS.view) { url.searchParams.delete("view"); } else { url.searchParams.set("view", state.view); } window.history.replaceState({}, "", url.toString()); } function formatMetric(value) { const raw = trimValue(value); if (!raw) { return "--"; } if (/^-?\d+(\.\d+)?$/.test(raw)) { return new Intl.NumberFormat("en-US").format(Number(raw)); } return raw; } function formatSignedReward(value) { const raw = trimValue(value); if (!raw) { return "+0"; } if (raw.startsWith("+")) { return raw; } return `+${formatMetric(raw)}`; } function buildInviteShareURL(inviteLink, inviteCode) { const rawURL = trimValue(inviteLink); const code = trimValue(inviteCode); if (!rawURL || !code) { return rawURL; } try { const url = new URL(rawURL, window.location.href); if (url.hostname.includes("play.google.com")) { const referrer = new URLSearchParams(url.searchParams.get("referrer") || ""); setPlayInstallReferrer(referrer, code); removeTopLevelInviteCodeParams(url.searchParams); url.searchParams.set("referrer", referrer.toString()); return url.toString(); } setInviteCodeParams(url.searchParams, code); return url.toString(); } catch { const separator = rawURL.includes("?") ? "&" : "?"; const params = new URLSearchParams(); setInviteCodeParams(params, code); return `${rawURL}${separator}${params.toString()}`; } } function currentInviteShareURL() { return buildInviteShareURL(state.inviteLink, state.inviteCode); } function currentInviteShareCopyText() { const inviteURL = trimValue(currentInviteShareURL()); return inviteURL ? `${INVITE_SHARE_COPY_PREFIX} ${inviteURL}` : ""; } function renderStats() { els.inviteLinkText.textContent = trimValue(currentInviteShareURL()) || "--"; els.inviteCodeText.textContent = trimValue(state.inviteCode) || "--"; els.statInvitees.textContent = formatMetric(state.invitees); els.statEligible.textContent = formatMetric(state.eligibleVoters); els.statRecharge.textContent = formatMetric(state.rechargeTotal); els.statRewards.textContent = formatMetric(state.myRewards); els.giftTitle.innerHTML = `Your friend [${escapeHtml(state.friendName)}]
has invited you to join`; setImageSource(els.giftAvatarImage, state.inviterAvatar, DEFAULT_AVATAR); if (els.inviteRewardAmounts[0]) { els.inviteRewardAmounts[0].textContent = formatSignedReward(state.inviterBindReward); } if (els.inviteRewardAmounts[1]) { els.inviteRewardAmounts[1].textContent = formatSignedReward(state.rechargeReward); } if (els.giftRewardAmount) { els.giftRewardAmount.textContent = formatSignedReward(state.giftReward); } } function renderTasks() { const current = state.tasks[state.taskSet] || cloneTasks(FALLBACK_TASKS[state.taskSet]); els.taskTabs.forEach((tab) => { const active = tab.getAttribute("data-task-set") === state.taskSet; tab.classList.toggle("active", active); tab.classList.toggle("passive", !active); }); if (current.length === 0) { els.taskList.innerHTML = `

No tasks yet

Task data will appear after the activity config is loaded.

`; els.taskNote.textContent = DEFAULT_TASK_NOTES[state.taskSet] || ""; return; } els.taskList.innerHTML = current .map((item) => { const ruleId = trimValue(item.ruleId); const isLoading = Boolean(ruleId) && state.pendingClaimRuleId === ruleId; const disabled = item.disabled || isLoading || !ruleId; const buttonLabel = isLoading ? "Loading..." : item.buttonLabel; let buttonStateClass = " is-pending"; if (isLoading) { buttonStateClass = " is-loading"; } else if (item.claimed) { buttonStateClass = " is-claimed"; } else if (item.achieved && !item.claimed) { buttonStateClass = " is-claimable"; } return `

${escapeHtml(item.title)}

${escapeHtml(item.desc)}

`; }) .join(""); els.taskNote.textContent = DEFAULT_TASK_NOTES[state.taskSet] || ""; } function rankBadgeMarkup(item) { const badgeMap = { 1: "./assets/rank-top1.png", 2: "./assets/rank-top2.png", 3: "./assets/rank-top3.png", }; const badge = badgeMap[item.rank]; return badge ? `Rank ${item.rank}` : escapeHtml(String(item.rank)); } function renderRankRows(container) { const list = state.rankItems; const inline = container?.dataset?.rankVariant === "inline"; if (!Array.isArray(list) || list.length === 0) { return inline ? `
--
No ranking data yet
0 person
0
` : `
--
No ranking data yet
0 people
`; } return list.map((item) => { if (inline) { return `
${rankBadgeMarkup(item)}
${escapeHtml(item.name)}
${escapeHtml(item.name)}
${escapeHtml(item.inviteCountText)}
${escapeHtml(item.rewardText)}
`; } return `
${rankBadgeMarkup(item)}
${escapeHtml(item.name)}
${escapeHtml(item.name)}
${escapeHtml(item.score)}
`; }).join(""); } function renderRankList() { const containers = els.rankLists.length > 0 ? els.rankLists : [els.rankList].filter(Boolean); containers.forEach((container) => { container.innerHTML = renderRankRows(container); }); } function pad(number) { return String(number).padStart(2, "0"); } function updateCountdown() { const now = Date.now() + state.serverOffsetMs; const diff = Math.max(0, Number(state.countdownEndAtMs || 0) - now); const seconds = Math.floor(diff / 1000); const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = seconds % 60; els.countdown.days.textContent = pad(days); els.countdown.hours.textContent = pad(hours); els.countdown.minutes.textContent = pad(minutes); els.countdown.seconds.textContent = pad(secs); } function setupCountdown() { updateCountdown(); state.countdownTimer = window.setInterval(updateCountdown, 1000); } async function copyText(text) { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); return; } const input = document.createElement("textarea"); input.value = text; input.setAttribute("readonly", ""); input.style.position = "absolute"; input.style.left = "-9999px"; document.body.appendChild(input); input.select(); document.execCommand("copy"); document.body.removeChild(input); } function extractErrorMessage(payload) { const candidates = [ payload?.errorMsg, payload?.message, payload?.errorCodeName, payload?.code, ]; return candidates.find((item) => trimValue(item)) || ""; } async function requestJSON(path, options = {}) { const { method = "GET", headers = {}, body, searchParams } = options; const url = buildAPIURL(path, searchParams); const requestHeaders = shouldAttachGatewayHeaders(path) ? state.requestHeaders : {}; const response = await fetch(url.toString(), { method, headers: { Accept: "application/json", ...requestHeaders, ...headers, }, body, }); const contentType = response.headers.get("content-type") || ""; const payload = contentType.includes("application/json") ? await response.json() : null; if (!response.ok || payload?.status === false) { const message = extractErrorMessage(payload) || `Request failed (${response.status})`; throw new Error(message); } return payload?.body || payload?.data || payload || null; } function shouldAttachGatewayHeaders(path) { const normalizedPath = joinURLPath(path); return !normalizedPath.startsWith("/public/"); } function joinURLPath(...parts) { return `/${parts .map((part) => trimValue(part).replace(/^\/+|\/+$/g, "")) .filter(Boolean) .join("/")}`; } function buildAPIURL(path, searchParams = {}) { const apiPath = joinURLPath(state.apiPrefix, path); let url; if (isAbsoluteURL(state.apiBase)) { url = new URL(apiPath.replace(/^\/+/, ""), state.apiBase); } else { url = new URL(joinURLPath(state.apiBase, apiPath), window.location.href); } Object.entries(searchParams || {}).forEach(([key, value]) => { const raw = trimValue(value); if (raw) { url.searchParams.set(key, raw); } }); return url; } function normalizeTaskList(list, taskSet) { if (!Array.isArray(list) || list.length === 0) { return cloneTasks(FALLBACK_TASKS[taskSet]); } return list.map((item, index) => { const threshold = Number(item?.threshold || 0); const goldAmount = Number(item?.goldAmount || 0); const achieved = Boolean(item?.achieved); const claimed = Boolean(item?.claimed); const desc = taskSet === "quota" ? `Invite ${formatMetric(threshold)} friends to claim ${formatMetric(goldAmount)} coins` : `Reach ${formatMetric(threshold)} recharge coins to claim ${formatMetric(goldAmount)} coins`; return { title: `Milestone ${index + 1}`, desc, buttonLabel: claimed ? "Claimed" : achieved ? "Claim" : "Incomplete", disabled: claimed || !achieved, claimed, achieved, ruleId: trimValue(item?.ruleId), }; }); } function normalizeRankList(list) { if (!Array.isArray(list)) { return []; } return list.map((item, index) => { const rank = Number(item?.rank || index + 1); const inviteCount = Number(item?.inviteCount || 0); const rewardGoldCoins = Number(item?.rewardGoldCoins || 0); const userName = trimValue(item?.nickname) || `User ${trimValue(item?.userId) || rank}`; const scoreParts = [`${formatMetric(inviteCount)} people`]; if (rewardGoldCoins > 0) { scoreParts.push(`${formatMetric(rewardGoldCoins)} coins`); } return { rank, name: userName, avatar: trimValue(item?.avatar) || DEFAULT_AVATAR, inviteCountText: `${formatMetric(inviteCount)} person`, rewardText: formatMetric(rewardGoldCoins), score: scoreParts.join(" ยท "), }; }); } function markTaskClaimed(ruleId) { const targetRuleId = trimValue(ruleId); ["quota", "transport"].forEach((taskSet) => { state.tasks[taskSet] = (state.tasks[taskSet] || []).map((item) => { if (trimValue(item.ruleId) !== targetRuleId) { return item; } return { ...item, claimed: true, disabled: true, buttonLabel: "Claimed", }; }); }); } function applyRankData(body) { if (!body || typeof body !== "object") { state.rankItems = []; return; } state.rankItems = normalizeRankList(body.items); } function applyHomeData(body) { if (!body || typeof body !== "object") { return; } const stats = body.stats || {}; if (body.serverTimeMs) { state.serverOffsetMs = Number(body.serverTimeMs) - Date.now(); } if (body.periodEndAtMs) { state.countdownEndAtMs = Number(body.periodEndAtMs); } state.inviteLink = trimValue(body.inviteLink) || state.inviteLink; state.inviteCode = trimValue(body.inviteCode) || state.inviteCode; state.downloadUrl = trimValue(body.downloadUrl) || state.downloadUrl; state.invitees = stats.totalInvitedUsers ?? state.invitees; state.eligibleVoters = stats.totalValidUsers ?? state.eligibleVoters; state.rechargeTotal = stats.totalRechargeCoins ?? state.rechargeTotal; state.myRewards = stats.rewardGoldCoins ?? state.myRewards; const inviterBindReward = body.inviterBindReward?.goldAmount; if (inviterBindReward != null) { state.inviterBindReward = inviterBindReward; } const rechargeReward = body.inviteeRechargeRules?.[0]?.goldAmount ?? body.inviterTotalRechargeTasks?.[0]?.goldAmount; if (rechargeReward != null) { state.rechargeReward = rechargeReward; } state.tasks.quota = normalizeTaskList(body.inviterValidCountTasks, "quota"); state.tasks.transport = normalizeTaskList(body.inviterTotalRechargeTasks, "transport"); const shareTitle = trimValue(body.shareTitle); if (shareTitle) { document.title = shareTitle; } } function applyLandingData(body) { if (!body || typeof body !== "object") { return; } state.friendName = trimValue(body.inviterNickname) || state.friendName; state.inviterAvatar = trimValue(body.inviterAvatar) || DEFAULT_AVATAR; state.downloadUrl = trimValue(body.downloadUrl) || state.downloadUrl; const inviteCode = trimValue(body.inviteCode); if (inviteCode) { state.landingCode = inviteCode; state.inviteCode = inviteCode; } const shareTitle = trimValue(body.shareTitle); if (shareTitle) { document.title = shareTitle; } } function buildDownloadURL(downloadUrl, inviteCode) { const rawURL = trimValue(downloadUrl); const code = trimValue(inviteCode); if (!rawURL || !code) { return rawURL; } try { const url = new URL(rawURL, window.location.href); if (url.hostname.includes("play.google.com")) { const referrer = new URLSearchParams(url.searchParams.get("referrer") || ""); setPlayInstallReferrer(referrer, code); removeTopLevelInviteCodeParams(url.searchParams); url.searchParams.set("referrer", referrer.toString()); return url.toString(); } setInviteCodeParams(url.searchParams, code); return url.toString(); } catch { const separator = rawURL.includes("?") ? "&" : "?"; const params = new URLSearchParams(); setInviteCodeParams(params, code); return `${rawURL}${separator}${params.toString()}`; } } function setInviteCodeParams(searchParams, code) { searchParams.set("code", code); searchParams.set("invitationCode", code); searchParams.set("inviteCode", code); } function setPlayInstallReferrer(searchParams, code) { searchParams.set("invite_code", code); searchParams.set("source", "share"); } function removeTopLevelInviteCodeParams(searchParams) { ["code", "invitationCode", "inviteCode", "invite_code"].forEach((name) => { searchParams.delete(name); }); } async function fetchHomeData() { if (!state.authorization) { return false; } const body = await requestJSON("/app/h5/invite-campaign/home", { headers: { Authorization: state.authorization, }, }); applyHomeData(body); return true; } async function fetchRankData() { if (!state.authorization) { return false; } const body = await requestJSON("/app/h5/invite-campaign/rank", { headers: { Authorization: state.authorization, }, searchParams: { limit: 50, }, }); applyRankData(body); return true; } async function fetchLandingData() { if (!state.landingCode) { return false; } const body = await requestJSON("/public/h5/invite-campaign/landing", { searchParams: { code: state.landingCode, }, }); applyLandingData(body); return true; } async function refreshPageData() { try { if (state.view === "gift") { await fetchLandingData(); } else { const results = await Promise.allSettled([ fetchHomeData(), fetchRankData(), ]); const rejected = results.find((item) => item.status === "rejected"); if (rejected) { throw rejected.reason; } } } catch (error) { const message = trimValue(error?.message); if (message) { showToast(message); } } finally { renderStats(); renderTasks(); renderRankList(); } } async function claimTask(ruleId) { const normalizedRuleId = trimValue(ruleId); if (!normalizedRuleId) { return; } if (!state.authorization) { showToast("Token is missing"); return; } state.pendingClaimRuleId = normalizedRuleId; renderTasks(); try { const body = await requestJSON("/app/h5/invite-campaign/tasks/claim", { method: "POST", headers: { Authorization: state.authorization, "Content-Type": "application/json", }, body: JSON.stringify({ ruleId: normalizedRuleId }), }); markTaskClaimed(normalizedRuleId); showToast(body?.alreadyClaimed ? "Already claimed" : "Claim succeeded"); await fetchHomeData().catch(() => {}); await fetchRankData().catch(() => {}); } catch (error) { showToast(trimValue(error?.message) || "Claim failed"); } finally { state.pendingClaimRuleId = ""; renderStats(); renderTasks(); renderRankList(); } } function bindEvents() { els.taskTabs.forEach((button) => { button.addEventListener("click", () => { state.taskSet = normalizeTaskSet(button.getAttribute("data-task-set")); renderTasks(); }); }); els.navButtons.forEach((button) => { button.addEventListener("click", () => { const nextView = button.getAttribute("data-nav-view") || "invite"; setView(nextView); if (nextView === "rank") { fetchRankData() .then(renderRankList) .catch((error) => showToast(trimValue(error?.message) || "Rank load failed")); } }); }); els.copyButtons.forEach((button) => { button.addEventListener("click", async () => { const target = button.getAttribute("data-copy-target"); const value = target === "invite-code" ? state.inviteCode : currentInviteShareCopyText(); try { await copyText(trimValue(value)); showToast(target === "invite-code" ? "Invitation code copied" : "Invitation link copied"); } catch (error) { showToast("Copy failed"); } }); }); els.taskList.addEventListener("click", (event) => { const button = event.target.closest("[data-rule-id]"); if (!(button instanceof HTMLButtonElement)) { return; } const ruleId = trimValue(button.getAttribute("data-rule-id")); if (!ruleId || button.disabled) { return; } claimTask(ruleId); }); els.downloadButton.addEventListener("click", () => { if (!state.downloadUrl) { showToast("Download link is not configured"); return; } window.location.href = buildDownloadURL(state.downloadUrl, state.landingCode || state.inviteCode); }); } async function init() { renderStats(); renderTasks(); renderRankList(); bindEvents(); setView(state.view); setupCountdown(); await connectToApp(); await refreshPageData(); } init();