300 lines
7.8 KiB
JavaScript
300 lines
7.8 KiB
JavaScript
const query = new URLSearchParams(window.location.search);
|
|
|
|
const LOCAL_PROXY_API_BASE = "/api/";
|
|
const LOCAL_FILE_PROXY_API_BASE = "http://127.0.0.1:8800/api/";
|
|
const LOCAL_INVITE_API_BASE = "http://127.0.0.1:2900/";
|
|
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 state = {
|
|
apiBase: resolveAPIBase(),
|
|
apiPrefix: resolveAPIPrefix(),
|
|
inviteCode: resolveInviteCode(),
|
|
friendName: trimValue(query.get("friendName")) || "Name",
|
|
inviterAvatar: trimValue(query.get("inviterAvatar")) || DEFAULT_AVATAR,
|
|
giftReward: INVITEE_DISPLAY_GIFT_REWARD,
|
|
downloadUrl: trimValue(query.get("downloadUrl")),
|
|
};
|
|
|
|
const els = {
|
|
title: document.getElementById("landingTitle"),
|
|
avatar: document.getElementById("inviterAvatar"),
|
|
giftRewardAmount: document.getElementById("giftRewardAmount"),
|
|
downloadButton: document.getElementById("downloadButton"),
|
|
toast: document.getElementById("pageToast"),
|
|
};
|
|
|
|
function trimValue(value) {
|
|
return value == null ? "" : String(value).trim();
|
|
}
|
|
|
|
function resolveInviteCode() {
|
|
return trimValue(
|
|
query.get("code") ||
|
|
query.get("landingCode") ||
|
|
query.get("invitationCode") ||
|
|
query.get("inviteCode"),
|
|
);
|
|
}
|
|
|
|
function isLocalHost(hostname = window.location.hostname) {
|
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
|
|
}
|
|
|
|
function isAbsoluteURL(value) {
|
|
return /^https?:\/\//i.test(trimValue(value));
|
|
}
|
|
|
|
function isOnlineAPIBase(base) {
|
|
try {
|
|
return new URL(base, window.location.href).origin === new URL(ONLINE_INVITE_API_BASE).origin;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function normalizeBaseURL(base) {
|
|
const value = trimValue(base);
|
|
const resolved = value || resolveDefaultAPIBase();
|
|
return resolved.endsWith("/") ? resolved : `${resolved}/`;
|
|
}
|
|
|
|
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 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 escapeHtml(value) {
|
|
return String(value).replace(/[&<>"']/g, (match) => {
|
|
const entityMap = {
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
'"': """,
|
|
"'": "'",
|
|
};
|
|
return entityMap[match] || match;
|
|
});
|
|
}
|
|
|
|
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 extractErrorMessage(payload) {
|
|
return (
|
|
payload?.errorMsg ||
|
|
payload?.message ||
|
|
payload?.errorCodeName ||
|
|
payload?.code ||
|
|
""
|
|
);
|
|
}
|
|
|
|
async function requestJSON(path, options = {}) {
|
|
const { searchParams } = options;
|
|
const response = await fetch(buildAPIURL(path, searchParams).toString(), {
|
|
method: "GET",
|
|
headers: {
|
|
Accept: "application/json",
|
|
},
|
|
});
|
|
const contentType = response.headers.get("content-type") || "";
|
|
const payload = contentType.includes("application/json") ? await response.json() : null;
|
|
|
|
if (!response.ok || payload?.status === false) {
|
|
throw new Error(extractErrorMessage(payload) || `Request failed (${response.status})`);
|
|
}
|
|
|
|
return payload?.body || payload?.data || payload || null;
|
|
}
|
|
|
|
function formatSignedReward(value) {
|
|
const number = Number(value || 0);
|
|
return `+${new Intl.NumberFormat("en-US").format(number)}`;
|
|
}
|
|
|
|
function render() {
|
|
els.title.innerHTML = `Your friend [${escapeHtml(state.friendName)}]<br />has invited you to join`;
|
|
els.avatar.src = trimValue(state.inviterAvatar) || DEFAULT_AVATAR;
|
|
// els.giftRewardAmount.textContent = formatSignedReward(state.giftReward);
|
|
}
|
|
|
|
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.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 loadLandingData() {
|
|
if (!state.inviteCode) {
|
|
showToast("Invitation code is missing");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const body = await requestJSON("/public/h5/invite-campaign/landing", {
|
|
searchParams: {
|
|
code: state.inviteCode,
|
|
},
|
|
});
|
|
applyLandingData(body);
|
|
} catch (error) {
|
|
showToast(trimValue(error?.message) || "Invite info load failed");
|
|
} finally {
|
|
render();
|
|
}
|
|
}
|
|
|
|
function bindEvents() {
|
|
els.downloadButton.addEventListener("click", () => {
|
|
if (!state.downloadUrl) {
|
|
showToast("Download link is not configured");
|
|
return;
|
|
}
|
|
|
|
window.location.href = buildDownloadURL(state.downloadUrl, state.inviteCode);
|
|
});
|
|
}
|
|
|
|
async function init() {
|
|
render();
|
|
bindEvents();
|
|
await loadLandingData();
|
|
}
|
|
|
|
init();
|