1058 lines
35 KiB
JavaScript
1058 lines
35 KiB
JavaScript
(function () {
|
|
const API_BASE_URL = "https://jvapi.haiyihy.com";
|
|
const RECHARGE_APPLICATION_ID = "1963531459019739137";
|
|
const supportedLanguages = ["en", "ar", "tr", "id"];
|
|
const languageLabels = {
|
|
en: "EN",
|
|
ar: "AR",
|
|
tr: "TR",
|
|
id: "ID"
|
|
};
|
|
|
|
const messages = {
|
|
en: {
|
|
document_title: "Recharge Center",
|
|
page_label: "Recharge Center",
|
|
back: "Back",
|
|
title: "Recharge Center",
|
|
language_button_aria: "Change language",
|
|
help: "Help",
|
|
close: "Close",
|
|
search_title: "Recharge user",
|
|
search_subtitle: "Search by user ID",
|
|
search_placeholder: "Enter user ID",
|
|
search: "Search",
|
|
searching: "Searching...",
|
|
target_user: "Target user",
|
|
uid_prefix: "UID:",
|
|
change: "Change",
|
|
select_country: "Select a country",
|
|
payment_methods: "Payment methods",
|
|
select_package: "Select a package",
|
|
loading_data: "Loading...",
|
|
no_country: "No supported country",
|
|
no_packages: "No recharge packages",
|
|
user_not_found: "User info not found",
|
|
missing_token: "Missing token",
|
|
failed_to_load: "Failed to load data",
|
|
payment_not_available: "Payment not available",
|
|
creating_order: "Creating order...",
|
|
redirecting_payment: "Redirecting to payment...",
|
|
order_created: "Order created",
|
|
faq_title: "FAQ",
|
|
faq_question_1: "How to recharge?",
|
|
faq_answer_1: "Enter the user's ID, choose a package, then select a payment method.",
|
|
faq_question_2: "How to check recharge result?",
|
|
faq_answer_2: "After payment, return to the app and check the user's wallet balance."
|
|
},
|
|
ar: {
|
|
document_title: "Recharge Center",
|
|
page_label: "Recharge Center",
|
|
back: "Back",
|
|
title: "Recharge Center",
|
|
language_button_aria: "Change language",
|
|
help: "Help",
|
|
close: "Close",
|
|
search_title: "Recharge user",
|
|
search_subtitle: "Search by user ID",
|
|
search_placeholder: "Enter user ID",
|
|
search: "Search",
|
|
searching: "Searching...",
|
|
target_user: "Target user",
|
|
uid_prefix: "UID:",
|
|
change: "Change",
|
|
select_country: "Select a country",
|
|
payment_methods: "Payment methods",
|
|
select_package: "Select a package",
|
|
loading_data: "Loading...",
|
|
no_country: "No supported country",
|
|
no_packages: "No recharge packages",
|
|
user_not_found: "User info not found",
|
|
missing_token: "Missing token",
|
|
failed_to_load: "Failed to load data",
|
|
payment_not_available: "Payment not available",
|
|
creating_order: "Creating order...",
|
|
redirecting_payment: "Redirecting to payment...",
|
|
order_created: "Order created",
|
|
faq_title: "FAQ",
|
|
faq_question_1: "How to recharge?",
|
|
faq_answer_1: "Enter the user's ID, choose a package, then select a payment method.",
|
|
faq_question_2: "How to check recharge result?",
|
|
faq_answer_2: "After payment, return to the app and check the user's wallet balance."
|
|
},
|
|
tr: {
|
|
document_title: "Recharge Center",
|
|
page_label: "Recharge Center",
|
|
back: "Geri",
|
|
title: "Recharge Center",
|
|
language_button_aria: "Dili degistir",
|
|
help: "Help",
|
|
close: "Kapat",
|
|
search_title: "Recharge user",
|
|
search_subtitle: "Kullanici ID ile ara",
|
|
search_placeholder: "Kullanici ID gir",
|
|
search: "Ara",
|
|
searching: "Araniyor...",
|
|
target_user: "Target user",
|
|
uid_prefix: "UID:",
|
|
change: "Degistir",
|
|
select_country: "Ulke sec",
|
|
payment_methods: "Payment methods",
|
|
select_package: "Paket sec",
|
|
loading_data: "Yukleniyor...",
|
|
no_country: "Desteklenen ulke yok",
|
|
no_packages: "Recharge package yok",
|
|
user_not_found: "Kullanici bulunamadi",
|
|
missing_token: "Token eksik",
|
|
failed_to_load: "Veri yuklenemedi",
|
|
payment_not_available: "Payment not available",
|
|
creating_order: "Order olusturuluyor...",
|
|
redirecting_payment: "Payment sayfasina yonlendiriliyor...",
|
|
order_created: "Order created",
|
|
faq_title: "FAQ",
|
|
faq_question_1: "How to recharge?",
|
|
faq_answer_1: "Enter the user's ID, choose a package, then select a payment method.",
|
|
faq_question_2: "How to check recharge result?",
|
|
faq_answer_2: "After payment, return to the app and check the user's wallet balance."
|
|
},
|
|
id: {
|
|
document_title: "Recharge Center",
|
|
page_label: "Recharge Center",
|
|
back: "Kembali",
|
|
title: "Recharge Center",
|
|
language_button_aria: "Ubah bahasa",
|
|
help: "Help",
|
|
close: "Tutup",
|
|
search_title: "Recharge user",
|
|
search_subtitle: "Cari dengan user ID",
|
|
search_placeholder: "Masukkan user ID",
|
|
search: "Cari",
|
|
searching: "Mencari...",
|
|
target_user: "Target user",
|
|
uid_prefix: "UID:",
|
|
change: "Ganti",
|
|
select_country: "Pilih negara",
|
|
payment_methods: "Payment methods",
|
|
select_package: "Pilih paket",
|
|
loading_data: "Memuat...",
|
|
no_country: "Negara tidak tersedia",
|
|
no_packages: "Paket recharge tidak tersedia",
|
|
user_not_found: "User tidak ditemukan",
|
|
missing_token: "Token tidak ditemukan",
|
|
failed_to_load: "Gagal memuat data",
|
|
payment_not_available: "Payment not available",
|
|
creating_order: "Membuat order...",
|
|
redirecting_payment: "Membuka payment...",
|
|
order_created: "Order created",
|
|
faq_title: "FAQ",
|
|
faq_question_1: "How to recharge?",
|
|
faq_answer_1: "Enter the user's ID, choose a package, then select a payment method.",
|
|
faq_question_2: "How to check recharge result?",
|
|
faq_answer_2: "After payment, return to the app and check the user's wallet balance."
|
|
}
|
|
};
|
|
|
|
const params = readURLParams();
|
|
let currentLang = normalizeLanguage(params.get("lang"));
|
|
let currentMessages = messages[currentLang] || messages.en;
|
|
|
|
const state = {
|
|
query: params.get("account") || params.get("targetAccount") || params.get("target") || "",
|
|
searchLocked: false,
|
|
loadingConfig: false,
|
|
ordering: false,
|
|
activeOrderKey: "",
|
|
searchStatusText: "",
|
|
searchStatusType: "",
|
|
paymentStatusText: "",
|
|
paymentStatusType: "",
|
|
profile: null,
|
|
application: null,
|
|
appCode: "",
|
|
rechargeType: "GOLD",
|
|
payProfile: null,
|
|
countryList: [],
|
|
selectedCountryId: "",
|
|
regionId: "",
|
|
commodity: [],
|
|
channels: []
|
|
};
|
|
|
|
function readURLParams() {
|
|
const result = new URLSearchParams(window.location.search);
|
|
const hashQuery = window.location.hash.includes("?") ? window.location.hash.split("?")[1] : "";
|
|
const hashParams = new URLSearchParams(hashQuery);
|
|
hashParams.forEach((value, key) => {
|
|
if (!result.has(key)) result.set(key, value);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
function readRawParam(name) {
|
|
const queries = [window.location.search.replace(/^\?/, "")];
|
|
if (window.location.hash.includes("?")) queries.push(window.location.hash.split("?")[1]);
|
|
|
|
for (const query of queries) {
|
|
for (const part of query.split("&")) {
|
|
if (!part) continue;
|
|
const equalIndex = part.indexOf("=");
|
|
const rawKey = equalIndex >= 0 ? part.slice(0, equalIndex) : part;
|
|
const rawValue = equalIndex >= 0 ? part.slice(equalIndex + 1) : "";
|
|
let decodedKey = "";
|
|
try {
|
|
decodedKey = decodeURIComponent(rawKey);
|
|
} catch (error) {
|
|
continue;
|
|
}
|
|
if (decodedKey !== name) continue;
|
|
try {
|
|
return decodeURIComponent(rawValue.replace(/\+/g, "%2B"));
|
|
} catch (error) {
|
|
return rawValue;
|
|
}
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function normalizeLanguage(lang) {
|
|
const value = String(lang || "en").toLowerCase();
|
|
if (value.startsWith("ar")) return "ar";
|
|
if (value.startsWith("tr")) return "tr";
|
|
if (value.startsWith("id")) return "id";
|
|
return supportedLanguages.includes(value) ? value : "en";
|
|
}
|
|
|
|
function message(key, fallback = "") {
|
|
return currentMessages[key] || messages.en[key] || fallback || key;
|
|
}
|
|
|
|
function renderMessages(dictionary) {
|
|
document.querySelectorAll("[data-i18n]").forEach((node) => {
|
|
const value = dictionary[node.dataset.i18n] || messages.en[node.dataset.i18n];
|
|
if (value) node.textContent = value;
|
|
});
|
|
|
|
document.querySelectorAll("[data-i18n-aria]").forEach((node) => {
|
|
const value = dictionary[node.dataset.i18nAria] || messages.en[node.dataset.i18nAria];
|
|
if (value) node.setAttribute("aria-label", value);
|
|
});
|
|
|
|
document.querySelectorAll("[data-i18n-placeholder]").forEach((node) => {
|
|
const value = dictionary[node.dataset.i18nPlaceholder] || messages.en[node.dataset.i18nPlaceholder];
|
|
if (value) node.setAttribute("placeholder", value);
|
|
});
|
|
}
|
|
|
|
function apiBaseURL() {
|
|
const override = window.HYAPP_API_BASE_URL || params.get("apiBase");
|
|
if (override) return String(override).replace(/\/+$/, "");
|
|
return API_BASE_URL.replace(/\/+$/, "");
|
|
}
|
|
|
|
function normalizeAuthorization(token) {
|
|
const value = String(token || "").trim();
|
|
if (!value) return "";
|
|
return /^bearer\s+/i.test(value) ? value : `Bearer ${value}`;
|
|
}
|
|
|
|
function authHeadersFromToken(token) {
|
|
return {
|
|
Authorization: normalizeAuthorization(token),
|
|
"req-lang": currentLang,
|
|
"req-client": "H5",
|
|
"req-version": "V2",
|
|
"req-zone": Intl.DateTimeFormat().resolvedOptions().timeZone || "Asia/Shanghai",
|
|
"req-app-intel": "version=1.0.0;build=1;model=H5;sysVersion=Web;channel=Web",
|
|
"req-sys-origin": "origin=ATYOU;originChild=ATYOU"
|
|
};
|
|
}
|
|
|
|
async function requestJSON(path, options = {}) {
|
|
const token = readRawParam("token");
|
|
if (!token) throw new Error(message("missing_token"));
|
|
|
|
const headers = authHeadersFromToken(token);
|
|
const request = {
|
|
method: options.method || "GET",
|
|
cache: "no-store",
|
|
headers
|
|
};
|
|
|
|
if (options.body !== undefined) {
|
|
headers["Content-Type"] = "application/json";
|
|
request.body = JSON.stringify(options.body);
|
|
}
|
|
|
|
const response = await fetch(new URL(path, `${apiBaseURL()}/`).toString(), request);
|
|
const data = await response.json().catch(() => ({}));
|
|
if (!response.ok || data.status === false) {
|
|
const error = new Error(data.errorMsg || data.message || `Request failed: ${response.status}`);
|
|
error.response = data;
|
|
throw error;
|
|
}
|
|
|
|
return data.body ?? data.data ?? null;
|
|
}
|
|
|
|
function endpointOpenSearch(account) {
|
|
return `/user/user-profile/open-search?account=${encodeURIComponent(account)}`;
|
|
}
|
|
|
|
function endpointApplication() {
|
|
return `/order/web/pay/application/${encodeURIComponent(RECHARGE_APPLICATION_ID)}`;
|
|
}
|
|
|
|
function endpointPayProfile(appCode, account, type) {
|
|
const query = new URLSearchParams({
|
|
sysOrigin: appCode,
|
|
account,
|
|
type
|
|
});
|
|
return `/order/web/pay/user-profile?${query.toString()}`;
|
|
}
|
|
|
|
function endpointCommodity(countryId, regionId, type) {
|
|
const query = new URLSearchParams({
|
|
applicationId: RECHARGE_APPLICATION_ID,
|
|
payCountryId: countryId,
|
|
regionId: regionId || "",
|
|
type
|
|
});
|
|
return `/order/web/pay/commodity?${query.toString()}`;
|
|
}
|
|
|
|
function setLoading(isLoading) {
|
|
const root = document.querySelector(".recharge-center");
|
|
const mask = document.querySelector("#loadingMask");
|
|
if (root) root.dataset.loading = isLoading ? "true" : "false";
|
|
if (mask) mask.hidden = !isLoading;
|
|
}
|
|
|
|
function updateModalLock() {
|
|
const hasOpen = Array.from(document.querySelectorAll(".help-modal")).some((modal) => !modal.hidden);
|
|
document.body.classList.toggle("modal-open", hasOpen);
|
|
}
|
|
|
|
function setModalOpen(selector, open) {
|
|
const modal = document.querySelector(selector);
|
|
if (!modal) return;
|
|
modal.hidden = !open;
|
|
updateModalLock();
|
|
}
|
|
|
|
function showToast(text) {
|
|
const toast = document.querySelector("#toast");
|
|
if (!toast) return;
|
|
toast.textContent = text || "";
|
|
toast.hidden = !text;
|
|
window.clearTimeout(showToast.timer);
|
|
showToast.timer = window.setTimeout(() => {
|
|
toast.hidden = true;
|
|
}, 2500);
|
|
}
|
|
|
|
function replaceChildren(node, children) {
|
|
while (node.firstChild) node.removeChild(node.firstChild);
|
|
children.forEach((child) => node.appendChild(child));
|
|
}
|
|
|
|
function formatMoney(value) {
|
|
const number = Number(value);
|
|
if (!Number.isFinite(number)) return "0.00";
|
|
return number.toFixed(2);
|
|
}
|
|
|
|
function pickSpecialAccount(profile) {
|
|
return profile?.ownSpecialId?.account || profile?.specialId?.account || profile?.specialAccount || profile?.shortAccount || profile?.shortId || "";
|
|
}
|
|
|
|
function pickProfileName(profile) {
|
|
return profile?.userNickname || profile?.name || "-";
|
|
}
|
|
|
|
function pickDisplayAccount(profile) {
|
|
return pickSpecialAccount(profile) || profile?.account || profile?.id || "-";
|
|
}
|
|
|
|
function pickApiAccount(profile) {
|
|
return profile?.account || state.query || pickSpecialAccount(profile) || "";
|
|
}
|
|
|
|
function pickProfileId(profile) {
|
|
return profile?.id || profile?.userId || "";
|
|
}
|
|
|
|
function commodityType(profile = state.profile) {
|
|
if (profile?.isSuperFreightAgent) return "FREIGHT_GOLD_SUPER";
|
|
if (profile?.isFreightAgent) return "FREIGHT_GOLD";
|
|
return "GOLD";
|
|
}
|
|
|
|
function setSearchStatus(text = "", type = "") {
|
|
state.searchStatusText = text;
|
|
state.searchStatusType = type;
|
|
const status = document.querySelector("#searchStatus");
|
|
if (!status) return;
|
|
status.textContent = text;
|
|
status.hidden = !text;
|
|
status.classList.toggle("is-error", type === "error");
|
|
}
|
|
|
|
function setPaymentStatus(text = "", type = "") {
|
|
state.paymentStatusText = text;
|
|
state.paymentStatusType = type;
|
|
const status = document.querySelector("#paymentStatus");
|
|
if (!status) return;
|
|
status.textContent = text;
|
|
status.hidden = !text;
|
|
status.classList.toggle("is-error", type === "error");
|
|
}
|
|
|
|
function renderSearch() {
|
|
const input = document.querySelector("#accountInput");
|
|
const button = document.querySelector("#searchButton");
|
|
if (input && document.activeElement !== input) input.value = state.query;
|
|
if (button) {
|
|
const hasQuery = Boolean(String(state.query || "").trim());
|
|
button.disabled = state.searchLocked || !hasQuery;
|
|
button.textContent = state.searchLocked ? message("searching") : message("search");
|
|
}
|
|
setSearchStatus(state.searchStatusText, state.searchStatusType);
|
|
}
|
|
|
|
function renderAvatar(profile) {
|
|
const image = document.querySelector("#targetAvatar");
|
|
const fallback = document.querySelector("#targetAvatarFallback");
|
|
if (!image || !fallback) return;
|
|
|
|
const name = pickProfileName(profile);
|
|
const avatar = profile?.userAvatar || profile?.avatar || "";
|
|
fallback.textContent = String(name || "U").slice(0, 1).toUpperCase() || "U";
|
|
|
|
if (!avatar) {
|
|
image.hidden = true;
|
|
image.removeAttribute("src");
|
|
fallback.hidden = false;
|
|
return;
|
|
}
|
|
|
|
image.alt = name;
|
|
fallback.hidden = true;
|
|
image.hidden = false;
|
|
image.onerror = () => {
|
|
image.hidden = true;
|
|
image.removeAttribute("src");
|
|
fallback.hidden = false;
|
|
};
|
|
image.src = avatar;
|
|
}
|
|
|
|
function renderTarget() {
|
|
const card = document.querySelector("#targetCard");
|
|
if (card) card.hidden = !state.profile;
|
|
if (!state.profile) return;
|
|
|
|
const nameNode = document.querySelector("#targetName");
|
|
const uidNode = document.querySelector("#targetUid");
|
|
if (nameNode) nameNode.textContent = pickProfileName(state.profile);
|
|
if (uidNode) uidNode.textContent = `${message("uid_prefix")} ${pickDisplayAccount(state.profile)}`.trim();
|
|
renderAvatar(state.profile);
|
|
}
|
|
|
|
function countryName(item) {
|
|
return item?.countryName || item?.country?.aliasName || item?.country?.countryName || item?.name || "-";
|
|
}
|
|
|
|
function countryFlag(item) {
|
|
return item?.nationalFlag || item?.country?.nationalFlag || item?.flag || "";
|
|
}
|
|
|
|
function selectedCountry() {
|
|
return state.countryList.find((item) => String(item?.id) === String(state.selectedCountryId)) || state.countryList[0] || null;
|
|
}
|
|
|
|
function createCountryChip(item) {
|
|
const button = document.createElement("button");
|
|
button.className = "country-chip";
|
|
button.type = "button";
|
|
const active = String(item?.id) === String(state.selectedCountryId);
|
|
button.classList.toggle("is-active", active);
|
|
button.disabled = state.loadingConfig || state.ordering;
|
|
button.addEventListener("click", () => selectCountry(item?.id));
|
|
|
|
const flag = document.createElement("span");
|
|
flag.className = "country-flag";
|
|
const flagUrl = countryFlag(item);
|
|
if (flagUrl) {
|
|
const image = document.createElement("img");
|
|
image.src = flagUrl;
|
|
image.alt = "";
|
|
image.addEventListener("error", () => {
|
|
flag.textContent = String(countryName(item)).slice(0, 2).toUpperCase();
|
|
});
|
|
flag.appendChild(image);
|
|
} else {
|
|
flag.textContent = String(countryName(item)).slice(0, 2).toUpperCase();
|
|
}
|
|
|
|
const name = document.createElement("span");
|
|
name.className = "country-name";
|
|
name.textContent = countryName(item);
|
|
button.append(flag, name);
|
|
return button;
|
|
}
|
|
|
|
function renderCountries() {
|
|
const card = document.querySelector("#countryCard");
|
|
const list = document.querySelector("#countryList");
|
|
const subtitle = document.querySelector("#countrySubtitle");
|
|
if (card) card.hidden = !state.profile;
|
|
if (!list || !state.profile) return;
|
|
|
|
const country = selectedCountry();
|
|
if (subtitle) subtitle.textContent = country ? countryName(country) : message("no_country");
|
|
|
|
if (state.loadingConfig && !state.countryList.length) {
|
|
const loading = document.createElement("div");
|
|
loading.className = "empty-state";
|
|
loading.textContent = message("loading_data");
|
|
replaceChildren(list, [loading]);
|
|
return;
|
|
}
|
|
|
|
if (!state.countryList.length) {
|
|
const empty = document.createElement("div");
|
|
empty.className = "empty-state";
|
|
empty.textContent = message("no_country");
|
|
replaceChildren(list, [empty]);
|
|
return;
|
|
}
|
|
|
|
replaceChildren(list, state.countryList.map(createCountryChip));
|
|
}
|
|
|
|
function channelCode(item) {
|
|
return item?.channel?.channelCode || item?.details?.channelCode || item?.channelCode || "";
|
|
}
|
|
|
|
function channelName(item) {
|
|
return item?.channel?.channelName || item?.channelName || channelCode(item) || "-";
|
|
}
|
|
|
|
function channelIcon(item) {
|
|
return item?.channel?.channelIcon || item?.channelIcon || "";
|
|
}
|
|
|
|
function createChannelIcon(item) {
|
|
const icon = document.createElement("div");
|
|
icon.className = "channel-icon";
|
|
const url = channelIcon(item);
|
|
if (!url) {
|
|
icon.textContent = String(channelName(item)).slice(0, 1).toUpperCase();
|
|
return icon;
|
|
}
|
|
const image = document.createElement("img");
|
|
image.src = url;
|
|
image.alt = "";
|
|
image.addEventListener("error", () => {
|
|
icon.textContent = String(channelName(item)).slice(0, 1).toUpperCase();
|
|
});
|
|
icon.appendChild(image);
|
|
return icon;
|
|
}
|
|
|
|
function packageKey(channel, goods) {
|
|
return `${channelCode(channel)}:${goods?.id || ""}`;
|
|
}
|
|
|
|
function createPackageButton(channel, goods) {
|
|
const button = document.createElement("button");
|
|
button.className = "package-button";
|
|
button.type = "button";
|
|
const key = packageKey(channel, goods);
|
|
button.disabled = state.ordering || state.loadingConfig;
|
|
button.addEventListener("click", () => placeOrder(channel, goods));
|
|
|
|
const coin = document.createElement("span");
|
|
coin.className = "coin-mark";
|
|
coin.textContent = "$";
|
|
const content = document.createElement("span");
|
|
content.className = "package-content";
|
|
content.textContent = goods?.content || goods?.awardContent || "-";
|
|
const price = document.createElement("span");
|
|
price.className = "package-price";
|
|
price.textContent = state.ordering && state.activeOrderKey === key
|
|
? message("creating_order")
|
|
: `$${formatMoney(goods?.amountUsd)}`;
|
|
|
|
button.append(coin, content, price);
|
|
return button;
|
|
}
|
|
|
|
function createChannelCard(item) {
|
|
const card = document.createElement("article");
|
|
card.className = "channel-card";
|
|
|
|
const head = document.createElement("div");
|
|
head.className = "channel-head";
|
|
const name = document.createElement("div");
|
|
name.className = "channel-name";
|
|
name.textContent = channelName(item);
|
|
head.append(createChannelIcon(item), name);
|
|
|
|
const grid = document.createElement("div");
|
|
grid.className = "package-grid";
|
|
state.commodity.forEach((goods) => grid.appendChild(createPackageButton(item, goods)));
|
|
|
|
card.append(head, grid);
|
|
return card;
|
|
}
|
|
|
|
function renderPayment() {
|
|
const card = document.querySelector("#paymentCard");
|
|
const list = document.querySelector("#channelList");
|
|
if (card) card.hidden = !state.profile;
|
|
if (!list || !state.profile) return;
|
|
|
|
setPaymentStatus(state.paymentStatusText, state.paymentStatusType);
|
|
|
|
if (state.loadingConfig) {
|
|
const loading = document.createElement("div");
|
|
loading.className = "empty-state";
|
|
loading.textContent = message("loading_data");
|
|
replaceChildren(list, [loading]);
|
|
return;
|
|
}
|
|
|
|
if (!state.channels.length || !state.commodity.length) {
|
|
const empty = document.createElement("div");
|
|
empty.className = "empty-state";
|
|
empty.textContent = message("no_packages");
|
|
replaceChildren(list, [empty]);
|
|
return;
|
|
}
|
|
|
|
replaceChildren(list, state.channels.map(createChannelCard));
|
|
}
|
|
|
|
function renderAll() {
|
|
renderSearch();
|
|
renderTarget();
|
|
renderCountries();
|
|
renderPayment();
|
|
}
|
|
|
|
async function fetchApplication() {
|
|
state.application = await requestJSON(endpointApplication()) || {};
|
|
state.appCode = state.application.appCode || "";
|
|
return state.application;
|
|
}
|
|
|
|
async function fetchRechargeProfile() {
|
|
if (!state.application || !state.appCode) await fetchApplication();
|
|
const account = pickApiAccount(state.profile);
|
|
state.rechargeType = commodityType(state.profile);
|
|
const data = await requestJSON(endpointPayProfile(state.appCode, account, state.rechargeType));
|
|
state.payProfile = data?.userProfile || null;
|
|
state.countryList = Array.isArray(data?.countryList) ? data.countryList : [];
|
|
state.regionId = data?.regionId || "";
|
|
if (state.payProfile) {
|
|
state.profile = {
|
|
...state.profile,
|
|
...state.payProfile,
|
|
isFreightAgent: state.profile?.isFreightAgent,
|
|
isSuperFreightAgent: state.profile?.isSuperFreightAgent
|
|
};
|
|
}
|
|
if (!state.countryList.some((item) => String(item?.id) === String(state.selectedCountryId))) {
|
|
state.selectedCountryId = state.countryList[0]?.id ? String(state.countryList[0].id) : "";
|
|
}
|
|
}
|
|
|
|
async function fetchCommodities() {
|
|
const country = selectedCountry();
|
|
if (!country?.id) {
|
|
state.commodity = [];
|
|
state.channels = [];
|
|
return;
|
|
}
|
|
const data = await requestJSON(endpointCommodity(String(country.id), state.regionId, state.rechargeType));
|
|
state.commodity = Array.isArray(data?.commodity) ? data.commodity : [];
|
|
state.channels = Array.isArray(data?.channels) ? data.channels : [];
|
|
if (data?.application) state.application = data.application;
|
|
}
|
|
|
|
async function loadRechargeOptions() {
|
|
state.loadingConfig = true;
|
|
state.paymentStatusText = message("loading_data");
|
|
state.paymentStatusType = "";
|
|
state.countryList = [];
|
|
state.selectedCountryId = "";
|
|
state.commodity = [];
|
|
state.channels = [];
|
|
renderAll();
|
|
|
|
try {
|
|
await fetchRechargeProfile();
|
|
await fetchCommodities();
|
|
setPaymentStatus("");
|
|
} catch (error) {
|
|
console.error("Failed to load recharge options:", error);
|
|
state.paymentStatusText = error.response?.errorMsg || error.message || message("failed_to_load");
|
|
state.paymentStatusType = "error";
|
|
showToast(state.paymentStatusText);
|
|
} finally {
|
|
state.loadingConfig = false;
|
|
renderAll();
|
|
}
|
|
}
|
|
|
|
async function searchTarget(event) {
|
|
if (event) event.preventDefault();
|
|
if (state.searchLocked) return;
|
|
|
|
const input = document.querySelector("#accountInput");
|
|
const query = String(input?.value || state.query || "").trim();
|
|
state.query = query;
|
|
if (!query) {
|
|
setSearchStatus("");
|
|
renderAll();
|
|
return;
|
|
}
|
|
|
|
state.searchLocked = true;
|
|
state.profile = null;
|
|
state.payProfile = null;
|
|
state.countryList = [];
|
|
state.selectedCountryId = "";
|
|
state.commodity = [];
|
|
state.channels = [];
|
|
setSearchStatus(message("searching"));
|
|
setPaymentStatus("");
|
|
renderAll();
|
|
|
|
try {
|
|
const result = await requestJSON(endpointOpenSearch(query));
|
|
const profile = Array.isArray(result) ? result[0] : result;
|
|
if (!profile || !pickProfileId(profile)) throw new Error(message("user_not_found"));
|
|
state.profile = profile;
|
|
setSearchStatus("");
|
|
renderAll();
|
|
await loadRechargeOptions();
|
|
} catch (error) {
|
|
console.error("Recharge target search failed:", error);
|
|
const text = error.response?.errorMsg || error.message || message("user_not_found");
|
|
setSearchStatus(text, "error");
|
|
showToast(text);
|
|
} finally {
|
|
state.searchLocked = false;
|
|
renderAll();
|
|
}
|
|
}
|
|
|
|
async function selectCountry(id) {
|
|
if (!id || String(id) === String(state.selectedCountryId) || state.loadingConfig || state.ordering) return;
|
|
|
|
state.selectedCountryId = String(id);
|
|
state.loadingConfig = true;
|
|
state.commodity = [];
|
|
state.channels = [];
|
|
state.paymentStatusText = message("loading_data");
|
|
state.paymentStatusType = "";
|
|
renderAll();
|
|
|
|
try {
|
|
await fetchCommodities();
|
|
setPaymentStatus("");
|
|
} catch (error) {
|
|
console.error("Failed to switch recharge country:", error);
|
|
state.paymentStatusText = error.response?.errorMsg || error.message || message("failed_to_load");
|
|
state.paymentStatusType = "error";
|
|
showToast(state.paymentStatusText);
|
|
} finally {
|
|
state.loadingConfig = false;
|
|
renderAll();
|
|
}
|
|
}
|
|
|
|
function payEnvironment() {
|
|
const value = String(window.AIRWALLEX_ENV || params.get("payEnv") || window.ENV || "").toLowerCase();
|
|
if (value === "production" || value === "prod") return "prod";
|
|
if (value === "staging") return "staging";
|
|
if (value === "dev") return "dev";
|
|
if (value === "local") return "local";
|
|
return "demo";
|
|
}
|
|
|
|
function airwallexHost(env) {
|
|
const hosts = {
|
|
prod: "checkout.airwallex.com",
|
|
demo: "checkout-demo.airwallex.com",
|
|
staging: "checkout-staging.airwallex.com",
|
|
dev: "checkout-dev.airwallex.com",
|
|
local: "localhost:3000"
|
|
};
|
|
return `https://${hosts[env] || hosts.prod}`;
|
|
}
|
|
|
|
function loadAirwallex(env) {
|
|
if (window.Airwallex) {
|
|
window.Airwallex.init({ env });
|
|
return Promise.resolve(window.Airwallex);
|
|
}
|
|
|
|
const src = `${airwallexHost(env)}/assets/elements.bundle.min.js?version=1.160.0`;
|
|
const existing = document.querySelector(`[data-airwallex-script="${env}"]`);
|
|
const script = existing || document.createElement("script");
|
|
if (!existing) {
|
|
script.src = src;
|
|
script.crossOrigin = "anonymous";
|
|
script.dataset.airwallexScript = env;
|
|
document.head.appendChild(script);
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
script.addEventListener("load", () => {
|
|
if (!window.Airwallex) {
|
|
reject(new Error("Failed to load Airwallex"));
|
|
return;
|
|
}
|
|
window.Airwallex.init({ env });
|
|
resolve(window.Airwallex);
|
|
}, { once: true });
|
|
script.addEventListener("error", () => reject(new Error("Failed to load Airwallex")), { once: true });
|
|
});
|
|
}
|
|
|
|
function parseExpand(value) {
|
|
if (!value) return {};
|
|
if (typeof value === "object") return value;
|
|
try {
|
|
return JSON.parse(value);
|
|
} catch (error) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function paymentResultURL(status) {
|
|
const type = params.get("type") || "";
|
|
const query = new URLSearchParams({
|
|
redirect: "recharge",
|
|
appType: type,
|
|
appId: RECHARGE_APPLICATION_ID,
|
|
appStatus: status
|
|
});
|
|
return `${window.location.origin}/#/pay-result?${query.toString()}`;
|
|
}
|
|
|
|
async function redirectAirwallex(result) {
|
|
const env = payEnvironment();
|
|
const expand = parseExpand(result.expand);
|
|
if (!expand.id || !expand.client_secret) throw new Error(message("payment_not_available"));
|
|
await loadAirwallex(env);
|
|
window.Airwallex.redirectToCheckout({
|
|
env,
|
|
mode: "payment",
|
|
currency: result.currency,
|
|
intent_id: expand.id,
|
|
client_secret: expand.client_secret,
|
|
methods: result.factoryChannelCode ? [result.factoryChannelCode] : undefined,
|
|
recurringOptions: {
|
|
card: {
|
|
next_triggered_by: "customer",
|
|
merchant_trigger_reason: "unscheduled",
|
|
currency: result.currency
|
|
}
|
|
},
|
|
logoUrl: window.sysOriginLog || "",
|
|
successUrl: paymentResultURL("0"),
|
|
failUrl: paymentResultURL("1"),
|
|
theme: {
|
|
fonts: [
|
|
{
|
|
src: "https://checkout.airwallex.com/fonts/CircularXXWeb/CircularXXWeb-Regular.woff2",
|
|
family: "AxLLCircular",
|
|
weight: 400
|
|
}
|
|
]
|
|
}
|
|
});
|
|
}
|
|
|
|
async function handlePaymentResponse(result) {
|
|
if (!result) throw new Error(message("payment_not_available"));
|
|
|
|
if (result.factoryCode === "AIRWALLEX") {
|
|
setPaymentStatus(message("redirecting_payment"));
|
|
await redirectAirwallex(result);
|
|
return;
|
|
}
|
|
|
|
if (result.requestUrl) {
|
|
setPaymentStatus(message("redirecting_payment"));
|
|
window.location.href = decodeURIComponent(result.requestUrl);
|
|
return;
|
|
}
|
|
|
|
setPaymentStatus(message("order_created"));
|
|
showToast(message("order_created"));
|
|
}
|
|
|
|
async function placeOrder(channel, goods) {
|
|
if (state.ordering || state.loadingConfig) return;
|
|
const country = selectedCountry();
|
|
const code = channelCode(channel);
|
|
const userId = pickProfileId(state.payProfile || state.profile);
|
|
|
|
if (!country?.id || !goods?.id || !userId || !code) {
|
|
showToast(message("payment_not_available"));
|
|
return;
|
|
}
|
|
|
|
state.ordering = true;
|
|
state.activeOrderKey = packageKey(channel, goods);
|
|
setPaymentStatus(message("creating_order"));
|
|
renderAll();
|
|
|
|
try {
|
|
const result = await requestJSON("/order/web/pay/recharge", {
|
|
method: "POST",
|
|
body: {
|
|
applicationId: RECHARGE_APPLICATION_ID,
|
|
payCountryId: String(country.id),
|
|
goodsId: String(goods.id),
|
|
userId: String(userId),
|
|
channelCode: code
|
|
}
|
|
});
|
|
await handlePaymentResponse(result);
|
|
} catch (error) {
|
|
console.error("Recharge order failed:", error);
|
|
const text = error.response?.errorMsg || error.message || message("payment_not_available");
|
|
setPaymentStatus(text, "error");
|
|
showToast(text);
|
|
} finally {
|
|
state.ordering = false;
|
|
state.activeOrderKey = "";
|
|
renderAll();
|
|
}
|
|
}
|
|
|
|
function changeTarget() {
|
|
state.profile = null;
|
|
state.payProfile = null;
|
|
state.countryList = [];
|
|
state.selectedCountryId = "";
|
|
state.regionId = "";
|
|
state.commodity = [];
|
|
state.channels = [];
|
|
state.paymentStatusText = "";
|
|
state.paymentStatusType = "";
|
|
renderAll();
|
|
window.setTimeout(() => document.querySelector("#accountInput")?.focus(), 60);
|
|
}
|
|
|
|
function closePage() {
|
|
try {
|
|
if (window.app && typeof window.app.closePage === "function") {
|
|
window.app.closePage();
|
|
return;
|
|
}
|
|
|
|
if (window.FlutterPageControl && typeof window.FlutterPageControl.postMessage === "function") {
|
|
window.FlutterPageControl.postMessage("close_page");
|
|
return;
|
|
}
|
|
|
|
if (window.history.length > 1) window.history.back();
|
|
} catch (error) {
|
|
console.error("close recharge center page failed:", error);
|
|
if (window.history.length > 1) window.history.back();
|
|
}
|
|
}
|
|
|
|
function setLanguageMenuOpen(open) {
|
|
const button = document.querySelector(".language-button");
|
|
const menu = document.querySelector(".language-menu");
|
|
if (!button || !menu) return;
|
|
menu.hidden = !open;
|
|
button.setAttribute("aria-expanded", String(open));
|
|
}
|
|
|
|
function renderLanguageControls() {
|
|
const button = document.querySelector(".language-button");
|
|
if (button) {
|
|
button.textContent = languageLabels[currentLang];
|
|
button.setAttribute("aria-label", message("language_button_aria", "Change language"));
|
|
}
|
|
document.querySelectorAll(".language-menu [data-lang]").forEach((node) => {
|
|
const active = node.dataset.lang === currentLang;
|
|
node.classList.toggle("is-active", active);
|
|
node.setAttribute("aria-pressed", String(active));
|
|
});
|
|
}
|
|
|
|
function applyLanguage(lang) {
|
|
currentLang = normalizeLanguage(lang);
|
|
currentMessages = messages[currentLang] || messages.en;
|
|
document.documentElement.lang = currentLang;
|
|
document.documentElement.dir = currentLang === "ar" ? "rtl" : "ltr";
|
|
document.title = message("document_title", "Recharge Center");
|
|
renderMessages(currentMessages);
|
|
renderLanguageControls();
|
|
renderAll();
|
|
}
|
|
|
|
function bindEvents() {
|
|
document.querySelector(".back-button")?.addEventListener("click", closePage);
|
|
document.querySelector(".language-button")?.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
const menu = document.querySelector(".language-menu");
|
|
setLanguageMenuOpen(Boolean(menu?.hidden));
|
|
});
|
|
document.querySelectorAll(".language-menu [data-lang]").forEach((button) => {
|
|
button.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
setLanguageMenuOpen(false);
|
|
applyLanguage(button.dataset.lang);
|
|
});
|
|
});
|
|
document.addEventListener("click", () => setLanguageMenuOpen(false));
|
|
document.querySelector("#accountInput")?.addEventListener("input", (event) => {
|
|
state.query = String(event.target.value || "");
|
|
if (!state.query.trim()) setSearchStatus("");
|
|
renderSearch();
|
|
});
|
|
document.querySelector("#searchForm")?.addEventListener("submit", searchTarget);
|
|
document.querySelector("[data-change-target]")?.addEventListener("click", changeTarget);
|
|
document.querySelector("[data-help-open]")?.addEventListener("click", () => setModalOpen("#helpModal", true));
|
|
document.querySelectorAll("[data-help-close]").forEach((node) => {
|
|
node.addEventListener("click", () => setModalOpen("#helpModal", false));
|
|
});
|
|
document.addEventListener("keydown", (event) => {
|
|
if (event.key === "Escape") setModalOpen("#helpModal", false);
|
|
});
|
|
}
|
|
|
|
async function initializePage() {
|
|
setLoading(true);
|
|
bindEvents();
|
|
applyLanguage(currentLang);
|
|
renderAll();
|
|
setLoading(false);
|
|
|
|
if (!readRawParam("token")) {
|
|
setSearchStatus(message("missing_token"), "error");
|
|
renderAll();
|
|
return;
|
|
}
|
|
|
|
if (state.query) {
|
|
await searchTarget();
|
|
}
|
|
}
|
|
|
|
initializePage();
|
|
})();
|