const query = new URLSearchParams(window.location.search); const DEFAULT_INVITE_API_BASE = "https://jvapi.haiyihy.com/"; const DEFAULT_AVATAR = "./assets/avatar-sample.png"; const DEFAULT_TASK_NOTES = { transport: "(Only the highest-level bonuses may be awarded)", quota: "(You'll receive a reward upon reaching each level)", }; const DEFAULTS = { view: "invite", inviteLink: "http://yourappbalbalanal", inviteCode: "123455678", invitees: "168", eligibleVoters: "01", rechargeTotal: "01", myRewards: "12354", friendName: "Name", downloadUrl: "https://example.com/download", endAt: "", inviterBindReward: 2000, rechargeReward: 1000, giftReward: 1000, }; const FALLBACK_TASKS = { transport: [ { title: "Milestone 1", desc: "Total invitee recharge reaches 49,999 coins", buttonLabel: "Incomplete", disabled: true, claimed: false, achieved: false, ruleId: 0, }, { title: "Milestone 2", desc: "Total invitee recharge reaches 99,999 coins", buttonLabel: "Incomplete", disabled: true, claimed: false, achieved: false, ruleId: 0, }, { title: "Milestone 3", desc: "Total invitee recharge reaches 149,999 coins", buttonLabel: "Incomplete", disabled: true, claimed: false, achieved: false, ruleId: 0, }, ], quota: [ { title: "Milestone 1", desc: "Invite 10 friends who have already made a payment", buttonLabel: "Claim", disabled: false, claimed: false, achieved: true, ruleId: 0, }, { title: "Milestone 2", desc: "Invite 20 friends who have already made a payment", buttonLabel: "Incomplete", disabled: true, claimed: false, achieved: false, ruleId: 0, }, { title: "Milestone 3", desc: "Invite 30 friends who have already made a payment", buttonLabel: "Claim", disabled: false, claimed: false, achieved: true, ruleId: 0, }, ], }; const DEFAULT_RANKS = Array.from({ length: 30 }, (_, index) => ({ rank: index + 1, name: [ "hanneem", "layan", "amira", "sami", "noura", "huda", "rami", "selim", "mira", "yael", ][index % 10], score: `${123 - Math.floor(index / 3)} people`, })); const state = { view: normalizeView(query.get("view") || DEFAULTS.view), apiBase: resolveAPIBase(), authorization: resolveAuthorization(), landingCode: trimValue(query.get("code") || query.get("inviteCode")), 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: query.get("giftReward") || DEFAULTS.giftReward, taskSet: normalizeTaskSet(query.get("taskSet") || "quota"), tasks: { transport: cloneTasks(FALLBACK_TASKS.transport), quota: cloneTasks(FALLBACK_TASKS.quota), }, countdownTimer: null, pendingClaimRuleId: 0, }; 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"), 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 normalizeBaseURL(base) { const value = trimValue(base); if (!value) { return DEFAULT_INVITE_API_BASE; } return value.endsWith("/") ? value : `${value}/`; } 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(DEFAULT_INVITE_API_BASE); } function resolveAuthorization() { const candidates = [ query.get("authorization"), query.get("Authorization"), query.get("token"), 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 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 renderStats() { els.inviteLinkText.textContent = trimValue(state.inviteLink) || "--"; 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); }); els.taskList.innerHTML = current .map((item) => { const isLoading = Number(item.ruleId) > 0 && state.pendingClaimRuleId === Number(item.ruleId); const disabled = item.disabled || isLoading || Number(item.ruleId) <= 0; 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 renderRankList() { els.rankList.innerHTML = DEFAULT_RANKS.map((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)) }
${escapeHtml(item.name)}
${escapeHtml(item.name)}
${escapeHtml(item.score)}
`; }).join(""); } function parseEndAt() { const raw = query.get("endAt") || DEFAULTS.endAt; if (!raw) { return null; } const timestamp = Date.parse(raw); if (Number.isNaN(timestamp)) { return null; } return timestamp; } function pad(number) { return String(number).padStart(2, "0"); } function updateCountdown() { const endAt = parseEndAt(); if (!endAt) { return; } const diff = Math.max(0, endAt - Date.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() { const endAt = parseEndAt(); if (!endAt) { return; } 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 = new URL(path, state.apiBase); Object.entries(searchParams || {}).forEach(([key, value]) => { const raw = trimValue(value); if (raw) { url.searchParams.set(key, raw); } }); const response = await fetch(url.toString(), { method, headers: { Accept: "application/json", ...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 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)} valid 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: Number(item?.ruleId) || 0, }; }); } function markTaskClaimed(ruleId) { ["quota", "transport"].forEach((taskSet) => { state.tasks[taskSet] = (state.tasks[taskSet] || []).map((item) => { if (Number(item.ruleId) !== Number(ruleId)) { return item; } return { ...item, claimed: true, disabled: true, buttonLabel: "Claimed", }; }); }); } function applyHomeData(body) { if (!body || typeof body !== "object") { return; } const stats = body.stats || {}; 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.inviterTotalRechargeTasks?.[0]?.goldAmount ?? body.inviteeRechargeRules?.[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; state.giftReward = body.inviteeReward?.goldAmount ?? state.giftReward; const inviteCode = trimValue(body.inviteCode); if (inviteCode) { state.landingCode = inviteCode; } const shareTitle = trimValue(body.shareTitle); if (shareTitle) { document.title = shareTitle; } } async function fetchHomeData() { if (!state.authorization) { return false; } const body = await requestJSON("/go/app/h5/invite-campaign/home", { headers: { Authorization: state.authorization, }, }); applyHomeData(body); return true; } async function fetchLandingData() { if (!state.landingCode) { return false; } const body = await requestJSON("/go/public/h5/invite-campaign/landing", { searchParams: { code: state.landingCode, }, }); applyLandingData(body); return true; } async function refreshPageData() { try { if (state.view === "gift") { await fetchLandingData(); } else { await fetchHomeData(); } } catch (error) { const message = trimValue(error?.message); if (message) { showToast(message); } } finally { renderStats(); renderTasks(); } } async function claimTask(ruleId) { if (!ruleId) { return; } if (!state.authorization) { showToast("Token is missing"); return; } state.pendingClaimRuleId = ruleId; renderTasks(); try { const body = await requestJSON("/go/app/h5/invite-campaign/tasks/claim", { method: "POST", headers: { Authorization: state.authorization, "Content-Type": "application/json", }, body: JSON.stringify({ ruleId }), }); markTaskClaimed(ruleId); showToast(body?.alreadyClaimed ? "Already claimed" : "Claim succeeded"); await fetchHomeData().catch(() => {}); } catch (error) { showToast(trimValue(error?.message) || "Claim failed"); } finally { state.pendingClaimRuleId = 0; renderStats(); renderTasks(); } } 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", () => { setView(button.getAttribute("data-nav-view") || "invite"); }); }); els.copyButtons.forEach((button) => { button.addEventListener("click", async () => { const target = button.getAttribute("data-copy-target"); const value = target === "invite-code" ? state.inviteCode : state.inviteLink; 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 = Number(button.getAttribute("data-rule-id") || 0); if (!ruleId || button.disabled) { return; } claimTask(ruleId); }); els.downloadButton.addEventListener("click", () => { if (!state.downloadUrl) { showToast("Download link is not configured"); return; } window.location.href = state.downloadUrl; }); } async function init() { renderStats(); renderTasks(); renderRankList(); bindEvents(); setView(state.view); setupCountdown(); await refreshPageData(); } init();