1773 lines
60 KiB
JavaScript
1773 lines
60 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",
|
|
my_account: "My account",
|
|
profile_card: "User profile",
|
|
whatsapp_label: "WhatsApp:",
|
|
add_whatsapp: "+ add whatsapp",
|
|
add_whatsapp_title: "Add WhatsApp",
|
|
whatsapp_placeholder: "Enter WhatsApp",
|
|
whatsapp_required: "Please enter WhatsApp",
|
|
whatsapp_too_long: "WhatsApp cannot exceed 64 characters",
|
|
whatsapp_saved: "WhatsApp saved",
|
|
save: "Save",
|
|
saving: "Saving...",
|
|
balance_title: "Coins",
|
|
history: "History",
|
|
search_title: "Recharge user",
|
|
search_subtitle: "Search by user ID",
|
|
search_placeholder: "Enter user ID",
|
|
search: "Search",
|
|
cancel: "Cancel",
|
|
searching: "Searching...",
|
|
target_user: "Target user",
|
|
uid_prefix: "UID:",
|
|
change: "Change",
|
|
coin_recharge_title: "Recharge coins",
|
|
coin_recharge_subtitle: "Enter coins amount",
|
|
coin_amount_placeholder: "Enter coins amount",
|
|
confirm_recharge: "Confirm recharge",
|
|
loading_data: "Loading...",
|
|
loading_mask: "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",
|
|
recharging: "Recharging...",
|
|
recharge_success: "Recharge success",
|
|
please_enter_valid_amount: "Please enter a valid amount",
|
|
transaction_records: "Transaction records",
|
|
income: "Income",
|
|
expenditure: "Expenditure",
|
|
enter_user_id: "Enter user ID",
|
|
coins: "coins",
|
|
no_transaction_records: "No transaction records",
|
|
no_selling_records_yet: "No records yet",
|
|
load_more: "Load more",
|
|
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, enter the coins amount, then confirm recharge.",
|
|
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: "مركز الشحن",
|
|
page_label: "مركز الشحن",
|
|
back: "رجوع",
|
|
title: "مركز الشحن",
|
|
language_button_aria: "تغيير اللغة",
|
|
help: "مساعدة",
|
|
close: "إغلاق",
|
|
my_account: "حسابي",
|
|
profile_card: "ملف المستخدم",
|
|
whatsapp_label: "WhatsApp:",
|
|
add_whatsapp: "+ إضافة واتساب",
|
|
add_whatsapp_title: "إضافة واتساب",
|
|
whatsapp_placeholder: "أدخل واتساب",
|
|
whatsapp_required: "يرجى إدخال واتساب",
|
|
whatsapp_too_long: "لا يمكن أن يتجاوز واتساب 64 حرفا",
|
|
whatsapp_saved: "تم حفظ واتساب",
|
|
save: "حفظ",
|
|
saving: "جار الحفظ...",
|
|
balance_title: "العملات",
|
|
history: "السجل",
|
|
search_title: "شحن مستخدم",
|
|
search_subtitle: "ابحث بواسطة معرف المستخدم",
|
|
search_placeholder: "أدخل معرف المستخدم",
|
|
search: "بحث",
|
|
cancel: "إلغاء",
|
|
searching: "جار البحث...",
|
|
target_user: "المستخدم المستهدف",
|
|
uid_prefix: "UID:",
|
|
change: "تغيير",
|
|
coin_recharge_title: "شحن العملات",
|
|
coin_recharge_subtitle: "أدخل عدد العملات",
|
|
coin_amount_placeholder: "أدخل عدد العملات",
|
|
confirm_recharge: "تأكيد الشحن",
|
|
loading_data: "جار التحميل...",
|
|
loading_mask: "جار التحميل....",
|
|
no_country: "لا توجد دولة مدعومة",
|
|
no_packages: "لا توجد باقات شحن",
|
|
user_not_found: "لم يتم العثور على المستخدم",
|
|
missing_token: "التوكن مفقود",
|
|
failed_to_load: "فشل تحميل البيانات",
|
|
recharging: "جار الشحن...",
|
|
recharge_success: "تم الشحن بنجاح",
|
|
please_enter_valid_amount: "يرجى إدخال مبلغ صالح",
|
|
transaction_records: "سجلات التحويل",
|
|
income: "الوارد",
|
|
expenditure: "الصادر",
|
|
enter_user_id: "أدخل معرف المستخدم",
|
|
coins: "عملات",
|
|
no_transaction_records: "لا توجد سجلات تحويل",
|
|
no_selling_records_yet: "لا توجد سجلات بعد",
|
|
load_more: "تحميل المزيد",
|
|
payment_not_available: "الدفع غير متاح",
|
|
creating_order: "جار إنشاء الطلب...",
|
|
redirecting_payment: "جار الانتقال إلى الدفع...",
|
|
order_created: "تم إنشاء الطلب",
|
|
faq_title: "الأسئلة الشائعة",
|
|
faq_question_1: "كيف أشحن؟",
|
|
faq_answer_1: "أدخل معرف المستخدم، ثم أدخل عدد العملات واضغط تأكيد الشحن.",
|
|
faq_question_2: "كيف أتحقق من نتيجة الشحن؟",
|
|
faq_answer_2: "بعد الشحن، تحقق من رصيد العملات في حساب المستخدم."
|
|
},
|
|
tr: {
|
|
document_title: "Yukleme Merkezi",
|
|
page_label: "Yukleme Merkezi",
|
|
back: "Geri",
|
|
title: "Yukleme Merkezi",
|
|
language_button_aria: "Dili degistir",
|
|
help: "Yardim",
|
|
close: "Kapat",
|
|
my_account: "Hesabim",
|
|
profile_card: "Kullanici profili",
|
|
whatsapp_label: "WhatsApp:",
|
|
add_whatsapp: "+ WhatsApp ekle",
|
|
add_whatsapp_title: "WhatsApp ekle",
|
|
whatsapp_placeholder: "WhatsApp gir",
|
|
whatsapp_required: "Lutfen WhatsApp girin",
|
|
whatsapp_too_long: "WhatsApp 64 karakteri gecemez",
|
|
whatsapp_saved: "WhatsApp kaydedildi",
|
|
save: "Kaydet",
|
|
saving: "Kaydediliyor...",
|
|
balance_title: "Coinler",
|
|
history: "Gecmis",
|
|
search_title: "Kullaniciya yukleme",
|
|
search_subtitle: "Kullanici ID ile ara",
|
|
search_placeholder: "Kullanici ID gir",
|
|
search: "Ara",
|
|
cancel: "Iptal",
|
|
searching: "Araniyor...",
|
|
target_user: "Hedef kullanici",
|
|
uid_prefix: "UID:",
|
|
change: "Degistir",
|
|
coin_recharge_title: "Coin yukle",
|
|
coin_recharge_subtitle: "Coin miktari gir",
|
|
coin_amount_placeholder: "Coin miktari gir",
|
|
confirm_recharge: "Yuklemeyi onayla",
|
|
loading_data: "Yukleniyor...",
|
|
loading_mask: "yukleniyor....",
|
|
no_country: "Desteklenen ulke yok",
|
|
no_packages: "Yukleme paketi yok",
|
|
user_not_found: "Kullanici bulunamadi",
|
|
missing_token: "Token eksik",
|
|
failed_to_load: "Veri yuklenemedi",
|
|
recharging: "Yukleniyor...",
|
|
recharge_success: "Yukleme basarili",
|
|
please_enter_valid_amount: "Gecerli bir miktar girin",
|
|
transaction_records: "Islem kayitlari",
|
|
income: "Gelir",
|
|
expenditure: "Gider",
|
|
enter_user_id: "Kullanici ID gir",
|
|
coins: "coin",
|
|
no_transaction_records: "Islem kaydi yok",
|
|
no_selling_records_yet: "Henuz kayit yok",
|
|
load_more: "Daha fazla yukle",
|
|
payment_not_available: "Odeme kullanilamiyor",
|
|
creating_order: "Siparis olusturuluyor...",
|
|
redirecting_payment: "Odeme sayfasina yonlendiriliyor...",
|
|
order_created: "Siparis olusturuldu",
|
|
faq_title: "SSS",
|
|
faq_question_1: "Nasil yukleme yapilir?",
|
|
faq_answer_1: "Kullanici ID girin, coin miktarini yazin ve yuklemeyi onaylayin.",
|
|
faq_question_2: "Yukleme sonucu nasil kontrol edilir?",
|
|
faq_answer_2: "Yuklemeden sonra kullanicinin coin bakiyesini kontrol edin."
|
|
},
|
|
id: {
|
|
document_title: "Pusat Isi Ulang",
|
|
page_label: "Pusat Isi Ulang",
|
|
back: "Kembali",
|
|
title: "Pusat Isi Ulang",
|
|
language_button_aria: "Ubah bahasa",
|
|
help: "Bantuan",
|
|
close: "Tutup",
|
|
my_account: "Akun saya",
|
|
profile_card: "Profil pengguna",
|
|
whatsapp_label: "WhatsApp:",
|
|
add_whatsapp: "+ tambah whatsapp",
|
|
add_whatsapp_title: "Tambah WhatsApp",
|
|
whatsapp_placeholder: "Masukkan WhatsApp",
|
|
whatsapp_required: "Masukkan WhatsApp",
|
|
whatsapp_too_long: "WhatsApp tidak boleh lebih dari 64 karakter",
|
|
whatsapp_saved: "WhatsApp tersimpan",
|
|
save: "Simpan",
|
|
saving: "Menyimpan...",
|
|
balance_title: "Koin",
|
|
history: "Riwayat",
|
|
search_title: "Isi ulang pengguna",
|
|
search_subtitle: "Cari dengan ID pengguna",
|
|
search_placeholder: "Masukkan ID pengguna",
|
|
search: "Cari",
|
|
cancel: "Batal",
|
|
searching: "Mencari...",
|
|
target_user: "Pengguna tujuan",
|
|
uid_prefix: "UID:",
|
|
change: "Ganti",
|
|
coin_recharge_title: "Isi ulang koin",
|
|
coin_recharge_subtitle: "Masukkan jumlah koin",
|
|
coin_amount_placeholder: "Masukkan jumlah koin",
|
|
confirm_recharge: "Konfirmasi isi ulang",
|
|
loading_data: "Memuat...",
|
|
loading_mask: "memuat....",
|
|
no_country: "Negara tidak tersedia",
|
|
no_packages: "Paket isi ulang tidak tersedia",
|
|
user_not_found: "User tidak ditemukan",
|
|
missing_token: "Token tidak ditemukan",
|
|
failed_to_load: "Gagal memuat data",
|
|
recharging: "Mengisi ulang...",
|
|
recharge_success: "Isi ulang berhasil",
|
|
please_enter_valid_amount: "Masukkan jumlah yang valid",
|
|
transaction_records: "Riwayat transaksi",
|
|
income: "Masuk",
|
|
expenditure: "Keluar",
|
|
enter_user_id: "Masukkan ID pengguna",
|
|
coins: "koin",
|
|
no_transaction_records: "Tidak ada riwayat transaksi",
|
|
no_selling_records_yet: "Belum ada riwayat",
|
|
load_more: "Muat lagi",
|
|
payment_not_available: "Pembayaran tidak tersedia",
|
|
creating_order: "Membuat pesanan...",
|
|
redirecting_payment: "Membuka pembayaran...",
|
|
order_created: "Pesanan dibuat",
|
|
faq_title: "FAQ",
|
|
faq_question_1: "Bagaimana cara isi ulang?",
|
|
faq_answer_1: "Masukkan ID pengguna, isi jumlah koin, lalu konfirmasi isi ulang.",
|
|
faq_question_2: "Bagaimana cara memeriksa hasil isi ulang?",
|
|
faq_answer_2: "Setelah isi ulang, periksa saldo koin pengguna."
|
|
}
|
|
};
|
|
|
|
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: "",
|
|
rechargeAmount: "",
|
|
rechargeLocked: false,
|
|
rechargeStatusText: "",
|
|
rechargeStatusType: "",
|
|
historyOpen: false,
|
|
historyLoading: false,
|
|
historyTab: 0,
|
|
historySearch: "",
|
|
historyStatusText: "",
|
|
historyStatusType: "",
|
|
historyIncome: [],
|
|
historyExpenditure: [],
|
|
historyHasMore: {
|
|
0: true,
|
|
1: true
|
|
},
|
|
searchStatusText: "",
|
|
searchStatusType: "",
|
|
paymentStatusText: "",
|
|
paymentStatusType: "",
|
|
selfProfile: null,
|
|
sellerProfile: null,
|
|
whatsapp: "",
|
|
whatsappOpen: false,
|
|
whatsappDraft: "",
|
|
whatsappSaving: false,
|
|
whatsappStatusText: "",
|
|
whatsappStatusType: "",
|
|
coins: 0,
|
|
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 goApiBaseURL() {
|
|
const override = window.HYAPP_GO_API_BASE_URL || params.get("goApiBase") || params.get("goBase");
|
|
if (override) return String(override).replace(/\/+$/, "");
|
|
return apiBaseURL();
|
|
}
|
|
|
|
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 requestBaseJSON(baseURL, 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, `${baseURL}/`).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;
|
|
}
|
|
|
|
async function requestJSON(path, options = {}) {
|
|
return requestBaseJSON(apiBaseURL(), path, options);
|
|
}
|
|
|
|
async function requestGoJSON(path, options = {}) {
|
|
return requestBaseJSON(goApiBaseURL(), path, options);
|
|
}
|
|
|
|
function endpointOpenSearch(account) {
|
|
return `/user/user-profile/open-search?account=${encodeURIComponent(account)}`;
|
|
}
|
|
|
|
function endpointSelfProfile() {
|
|
return "/team/member/profile";
|
|
}
|
|
|
|
function endpointSellerProfile() {
|
|
return "/app/h5/recharge-agency/profile";
|
|
}
|
|
|
|
function endpointSellerWhatsapp() {
|
|
return "/app/h5/recharge-agency/profile/whatsapp";
|
|
}
|
|
|
|
function endpointCoinBalance() {
|
|
return "/wallet/freight/balance";
|
|
}
|
|
|
|
function endpointFreightShip() {
|
|
return "/wallet/freight/ship";
|
|
}
|
|
|
|
function endpointTransferHistory({ userId, type, searchId = "", lastId = "" }) {
|
|
const query = new URLSearchParams({
|
|
userId: String(userId || ""),
|
|
type: String(type),
|
|
searchId: String(searchId || ""),
|
|
lastId: String(lastId || "")
|
|
});
|
|
return `/wallet/freight/balance/running/water/client/flowByUserId?${query.toString()}`;
|
|
}
|
|
|
|
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, .history-modal, .whatsapp-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 pickCoinAmount(body) {
|
|
if (typeof body === "number" || typeof body === "string") return body;
|
|
if (body && typeof body === "object") {
|
|
return body.gold ?? body.goldBalance ?? body.coin ?? body.coins ?? body.balance ?? body.amount ?? 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function pickWhatsapp(body) {
|
|
if (typeof body === "string") return body.trim();
|
|
if (body && typeof body === "object") {
|
|
return String(body.whatsapp || body.whatsApp || body.contactInfo || "").trim();
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function formatCoins(value) {
|
|
const number = Number(value);
|
|
if (!Number.isFinite(number)) return "0";
|
|
return Number.isInteger(number) ? String(number) : String(Number(number.toFixed(2)));
|
|
}
|
|
|
|
function normalizeRecordList(body) {
|
|
if (Array.isArray(body)) return body;
|
|
if (Array.isArray(body?.records)) return body.records;
|
|
if (Array.isArray(body?.list)) return body.list;
|
|
if (Array.isArray(body?.content)) return body.content;
|
|
if (Array.isArray(body?.rows)) return body.rows;
|
|
return [];
|
|
}
|
|
|
|
function formatRecordTime(value) {
|
|
if (!value) return "-";
|
|
const date = new Date(Number(value) || value);
|
|
if (Number.isNaN(date.getTime())) return String(value);
|
|
const pad = (item) => String(item).padStart(2, "0");
|
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
}
|
|
|
|
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 pickRecordUser(record) {
|
|
if (Number(record?.type) === 1) {
|
|
return record?.acceptUserAccount || record?.acceptUser || record?.targetUser || {};
|
|
}
|
|
return record?.userAccount || record?.user || record?.sourceUser || {};
|
|
}
|
|
|
|
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 setRechargeStatus(text = "", type = "") {
|
|
state.rechargeStatusText = text;
|
|
state.rechargeStatusType = type;
|
|
const status = document.querySelector("#coinRechargeStatus");
|
|
if (!status) return;
|
|
status.textContent = text;
|
|
status.hidden = !text;
|
|
status.classList.toggle("is-error", type === "error");
|
|
}
|
|
|
|
function setHistoryStatus(text = "", type = "") {
|
|
state.historyStatusText = text;
|
|
state.historyStatusType = type;
|
|
const status = document.querySelector("#historyStatus");
|
|
if (!status) return;
|
|
status.textContent = text;
|
|
status.hidden = !text;
|
|
status.classList.toggle("is-error", type === "error");
|
|
}
|
|
|
|
function setWhatsappStatus(text = "", type = "") {
|
|
state.whatsappStatusText = text;
|
|
state.whatsappStatusType = type;
|
|
const status = document.querySelector("#whatsappStatus");
|
|
if (!status) return;
|
|
status.textContent = text;
|
|
status.hidden = !text;
|
|
status.classList.toggle("is-error", type === "error");
|
|
}
|
|
|
|
function sanitizeCoinAmount(value) {
|
|
return String(value || "").replace(/\D/g, "").replace(/^0+(?=\d)/, "");
|
|
}
|
|
|
|
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 renderProfileAvatar(profile, imageSelector, fallbackSelector, defaultLetter = "U") {
|
|
const image = document.querySelector(imageSelector);
|
|
const fallback = document.querySelector(fallbackSelector);
|
|
if (!image || !fallback) return;
|
|
|
|
const name = pickProfileName(profile);
|
|
const avatar = profile?.userAvatar || profile?.avatar || "";
|
|
fallback.textContent = String(name || defaultLetter).slice(0, 1).toUpperCase() || defaultLetter;
|
|
|
|
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 renderSelfProfile() {
|
|
const profile = state.selfProfile || {};
|
|
const nameNode = document.querySelector("#selfName");
|
|
const uidNode = document.querySelector("#selfUid");
|
|
if (nameNode) nameNode.textContent = pickProfileName(profile);
|
|
if (uidNode) uidNode.textContent = `${message("uid_prefix")} ${pickDisplayAccount(profile)}`.trim();
|
|
renderProfileAvatar(profile, "#selfAvatar", "#selfAvatarFallback");
|
|
renderSelfWhatsapp();
|
|
}
|
|
|
|
function renderSelfWhatsapp() {
|
|
const icon = document.querySelector("#selfWhatsappIcon");
|
|
const text = document.querySelector("#selfWhatsappText");
|
|
const button = document.querySelector("#addWhatsappButton");
|
|
const whatsapp = String(state.whatsapp || "").trim();
|
|
if (icon) icon.hidden = !whatsapp;
|
|
if (text) {
|
|
text.textContent = whatsapp;
|
|
text.hidden = !whatsapp;
|
|
}
|
|
if (button) {
|
|
button.hidden = Boolean(whatsapp);
|
|
button.textContent = message("add_whatsapp");
|
|
}
|
|
}
|
|
|
|
function renderBalance() {
|
|
const node = document.querySelector("#selfBalance");
|
|
if (node) node.textContent = formatCoins(state.coins);
|
|
}
|
|
|
|
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();
|
|
renderProfileAvatar(state.profile, "#targetAvatar", "#targetAvatarFallback");
|
|
}
|
|
|
|
function renderCoinRecharge() {
|
|
const card = document.querySelector("#coinRechargeCard");
|
|
const input = document.querySelector("#coinAmountInput");
|
|
const button = document.querySelector("#confirmRechargeButton");
|
|
const amount = Number(state.rechargeAmount);
|
|
const canSubmit = Boolean(state.profile && Number.isFinite(amount) && amount > 0 && !state.rechargeLocked);
|
|
|
|
if (card) card.hidden = !state.profile;
|
|
if (input && document.activeElement !== input) input.value = state.rechargeAmount;
|
|
if (button) {
|
|
button.disabled = !canSubmit;
|
|
button.textContent = state.rechargeLocked ? message("recharging") : message("confirm_recharge");
|
|
}
|
|
setRechargeStatus(state.rechargeStatusText, state.rechargeStatusType);
|
|
}
|
|
|
|
function activeHistoryList() {
|
|
return state.historyTab === 1 ? state.historyExpenditure : state.historyIncome;
|
|
}
|
|
|
|
function setActiveHistoryList(records) {
|
|
if (state.historyTab === 1) state.historyExpenditure = records;
|
|
else state.historyIncome = records;
|
|
}
|
|
|
|
function createHistoryAvatar(user) {
|
|
const avatar = document.createElement("div");
|
|
avatar.className = "history-avatar";
|
|
const url = user?.userAvatar || user?.avatar || "";
|
|
const name = user?.userNickName || user?.userNickname || user?.name || "-";
|
|
if (url) {
|
|
const image = document.createElement("img");
|
|
image.src = url;
|
|
image.alt = "";
|
|
image.addEventListener("error", () => {
|
|
avatar.textContent = String(name).slice(0, 1).toUpperCase() || "U";
|
|
});
|
|
avatar.appendChild(image);
|
|
} else {
|
|
avatar.textContent = String(name).slice(0, 1).toUpperCase() || "U";
|
|
}
|
|
return avatar;
|
|
}
|
|
|
|
function createHistoryRecord(record) {
|
|
const user = pickRecordUser(record);
|
|
const item = document.createElement("article");
|
|
item.className = "history-record";
|
|
|
|
const userWrap = document.createElement("div");
|
|
userWrap.className = "history-record-user";
|
|
const copy = document.createElement("div");
|
|
copy.className = "history-record-copy";
|
|
const name = document.createElement("div");
|
|
name.className = "history-record-name";
|
|
name.textContent = user?.userNickName || user?.userNickname || user?.name || "-";
|
|
const account = document.createElement("div");
|
|
account.className = "history-record-account";
|
|
account.textContent = `${message("uid_prefix")} ${user?.account || user?.id || "-"}`.trim();
|
|
copy.append(name, account);
|
|
userWrap.append(createHistoryAvatar(user), copy);
|
|
|
|
const side = document.createElement("div");
|
|
side.className = "history-record-side";
|
|
const amount = document.createElement("div");
|
|
const isIncome = Number(record?.type) === 0;
|
|
amount.className = `history-record-amount ${isIncome ? "is-income" : "is-expense"}`;
|
|
amount.textContent = `${isIncome ? "+" : "-"}${formatCoins(record?.quantity)} ${message("coins")}`;
|
|
const time = document.createElement("div");
|
|
time.className = "history-record-time";
|
|
time.textContent = formatRecordTime(record?.createTime);
|
|
side.append(amount, time);
|
|
|
|
item.append(userWrap, side);
|
|
return item;
|
|
}
|
|
|
|
function renderHistoryModal() {
|
|
const modal = document.querySelector("#historyModal");
|
|
const input = document.querySelector("#historySearchInput");
|
|
const list = document.querySelector("#historyList");
|
|
const loadMore = document.querySelector("#historyLoadMore");
|
|
if (modal) modal.hidden = !state.historyOpen;
|
|
if (input && document.activeElement !== input) input.value = state.historySearch;
|
|
|
|
document.querySelectorAll("[data-history-tab]").forEach((button) => {
|
|
const active = Number(button.dataset.historyTab) === state.historyTab;
|
|
button.classList.toggle("is-active", active);
|
|
button.setAttribute("aria-selected", String(active));
|
|
});
|
|
|
|
setHistoryStatus(state.historyStatusText, state.historyStatusType);
|
|
|
|
if (list) {
|
|
const records = activeHistoryList();
|
|
if (state.historyLoading && !records.length) {
|
|
const loading = document.createElement("div");
|
|
loading.className = "history-empty";
|
|
loading.textContent = message("loading_data");
|
|
replaceChildren(list, [loading]);
|
|
} else if (!records.length) {
|
|
const empty = document.createElement("div");
|
|
empty.className = "history-empty";
|
|
const title = document.createElement("strong");
|
|
title.textContent = message("no_transaction_records");
|
|
const text = document.createElement("span");
|
|
text.textContent = message("no_selling_records_yet");
|
|
empty.append(title, text);
|
|
replaceChildren(list, [empty]);
|
|
} else {
|
|
replaceChildren(list, records.map(createHistoryRecord));
|
|
}
|
|
}
|
|
|
|
if (loadMore) {
|
|
const hasRecords = activeHistoryList().length > 0;
|
|
loadMore.hidden = state.historyLoading || !hasRecords || !state.historyHasMore[state.historyTab];
|
|
loadMore.disabled = state.historyLoading;
|
|
}
|
|
updateModalLock();
|
|
}
|
|
|
|
function renderWhatsappModal() {
|
|
const modal = document.querySelector("#whatsappModal");
|
|
const input = document.querySelector("#whatsappInput");
|
|
const button = document.querySelector("#saveWhatsappButton");
|
|
if (modal) modal.hidden = !state.whatsappOpen;
|
|
if (input && document.activeElement !== input) input.value = state.whatsappDraft;
|
|
if (button) {
|
|
button.disabled = state.whatsappSaving || !String(state.whatsappDraft || "").trim();
|
|
button.textContent = state.whatsappSaving ? message("saving") : message("save");
|
|
}
|
|
setWhatsappStatus(state.whatsappStatusText, state.whatsappStatusType);
|
|
updateModalLock();
|
|
}
|
|
|
|
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 gold-coin-icon";
|
|
const shine = document.createElement("span");
|
|
shine.className = "gold-coin-shine";
|
|
coin.appendChild(shine);
|
|
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() {
|
|
renderSelfProfile();
|
|
renderBalance();
|
|
renderSearch();
|
|
renderTarget();
|
|
renderCoinRecharge();
|
|
renderWhatsappModal();
|
|
}
|
|
|
|
async function fetchSelfProfile() {
|
|
state.selfProfile = await requestJSON(endpointSelfProfile()) || {};
|
|
renderSelfProfile();
|
|
return state.selfProfile;
|
|
}
|
|
|
|
async function fetchSellerProfile() {
|
|
state.sellerProfile = await requestGoJSON(endpointSellerProfile()) || {};
|
|
state.whatsapp = pickWhatsapp(state.sellerProfile);
|
|
renderSelfWhatsapp();
|
|
return state.sellerProfile;
|
|
}
|
|
|
|
async function fetchCoinBalance() {
|
|
const body = await requestJSON(endpointCoinBalance());
|
|
state.coins = pickCoinAmount(body);
|
|
renderBalance();
|
|
return state.coins;
|
|
}
|
|
|
|
function resetHistoryCache() {
|
|
state.historyIncome = [];
|
|
state.historyExpenditure = [];
|
|
state.historyHasMore = {
|
|
0: true,
|
|
1: true
|
|
};
|
|
state.historyStatusText = "";
|
|
state.historyStatusType = "";
|
|
}
|
|
|
|
async function fetchHistoryRecords({ append = false } = {}) {
|
|
const userId = pickProfileId(state.selfProfile);
|
|
if (!userId) {
|
|
setHistoryStatus(message("user_not_found"), "error");
|
|
renderHistoryModal();
|
|
return;
|
|
}
|
|
|
|
const currentRecords = activeHistoryList();
|
|
const lastId = append && currentRecords.length ? currentRecords[currentRecords.length - 1]?.id || "" : "";
|
|
state.historyLoading = true;
|
|
setHistoryStatus(message("loading_data"));
|
|
renderHistoryModal();
|
|
|
|
try {
|
|
const body = await requestJSON(endpointTransferHistory({
|
|
userId,
|
|
type: state.historyTab,
|
|
searchId: state.historySearch,
|
|
lastId
|
|
}));
|
|
const records = normalizeRecordList(body);
|
|
setActiveHistoryList(append ? currentRecords.concat(records) : records);
|
|
state.historyHasMore[state.historyTab] = records.length > 0;
|
|
setHistoryStatus("");
|
|
} catch (error) {
|
|
console.error("Failed to load transfer history:", error);
|
|
const text = error.response?.errorMsg || error.message || message("failed_to_load");
|
|
setHistoryStatus(text, "error");
|
|
showToast(text);
|
|
} finally {
|
|
state.historyLoading = false;
|
|
renderHistoryModal();
|
|
}
|
|
}
|
|
|
|
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.rechargeAmount = "";
|
|
state.rechargeStatusText = "";
|
|
state.rechargeStatusType = "";
|
|
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;
|
|
state.rechargeAmount = "";
|
|
state.rechargeStatusText = "";
|
|
state.rechargeStatusType = "";
|
|
setSearchStatus("");
|
|
renderAll();
|
|
} 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();
|
|
}
|
|
}
|
|
|
|
async function rechargeCoins(event) {
|
|
if (event) event.preventDefault();
|
|
if (state.rechargeLocked) return;
|
|
|
|
const input = document.querySelector("#coinAmountInput");
|
|
const amountValue = sanitizeCoinAmount(input?.value || state.rechargeAmount);
|
|
state.rechargeAmount = amountValue;
|
|
if (input) input.value = amountValue;
|
|
|
|
const quantity = Number(amountValue);
|
|
const userId = pickProfileId(state.profile);
|
|
if (!userId) {
|
|
const text = message("user_not_found");
|
|
setRechargeStatus(text, "error");
|
|
showToast(text);
|
|
renderAll();
|
|
return;
|
|
}
|
|
|
|
if (!Number.isFinite(quantity) || quantity <= 0) {
|
|
const text = message("please_enter_valid_amount");
|
|
setRechargeStatus(text, "error");
|
|
showToast(text);
|
|
renderAll();
|
|
return;
|
|
}
|
|
|
|
state.rechargeLocked = true;
|
|
setRechargeStatus(message("recharging"));
|
|
renderAll();
|
|
|
|
try {
|
|
const result = await requestJSON(endpointFreightShip(), {
|
|
method: "POST",
|
|
body: {
|
|
acceptUserId: String(userId),
|
|
quantity
|
|
}
|
|
});
|
|
if (result !== null && result !== undefined) {
|
|
state.coins = pickCoinAmount(result);
|
|
renderBalance();
|
|
} else {
|
|
await fetchCoinBalance();
|
|
}
|
|
state.rechargeAmount = "";
|
|
resetHistoryCache();
|
|
setRechargeStatus(message("recharge_success"));
|
|
showToast(message("recharge_success"));
|
|
if (state.historyOpen) fetchHistoryRecords();
|
|
} catch (error) {
|
|
console.error("Recharge coins failed:", error);
|
|
const text = error.response?.errorMsg || error.message || message("failed_to_load");
|
|
setRechargeStatus(text, "error");
|
|
showToast(text);
|
|
} finally {
|
|
state.rechargeLocked = 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.rechargeAmount = "";
|
|
state.rechargeStatusText = "";
|
|
state.rechargeStatusType = "";
|
|
state.countryList = [];
|
|
state.selectedCountryId = "";
|
|
state.regionId = "";
|
|
state.commodity = [];
|
|
state.channels = [];
|
|
state.paymentStatusText = "";
|
|
state.paymentStatusType = "";
|
|
renderAll();
|
|
window.setTimeout(() => document.querySelector("#accountInput")?.focus(), 60);
|
|
}
|
|
|
|
async function openHistoryModal() {
|
|
state.historyOpen = true;
|
|
renderHistoryModal();
|
|
if (!activeHistoryList().length && !state.historyLoading) {
|
|
await fetchHistoryRecords();
|
|
}
|
|
}
|
|
|
|
function closeHistoryModal() {
|
|
state.historyOpen = false;
|
|
renderHistoryModal();
|
|
}
|
|
|
|
async function switchHistoryTab(type) {
|
|
const nextType = Number(type);
|
|
if (state.historyTab === nextType || state.historyLoading) return;
|
|
state.historyTab = nextType;
|
|
state.historyStatusText = "";
|
|
state.historyStatusType = "";
|
|
renderHistoryModal();
|
|
if (!activeHistoryList().length) await fetchHistoryRecords();
|
|
}
|
|
|
|
async function searchHistory(event) {
|
|
if (event) event.preventDefault();
|
|
state.historySearch = String(document.querySelector("#historySearchInput")?.value || "").trim();
|
|
resetHistoryCache();
|
|
renderHistoryModal();
|
|
await fetchHistoryRecords();
|
|
}
|
|
|
|
async function clearHistorySearch() {
|
|
if (!state.historySearch && !document.querySelector("#historySearchInput")?.value) return;
|
|
state.historySearch = "";
|
|
resetHistoryCache();
|
|
renderHistoryModal();
|
|
await fetchHistoryRecords();
|
|
}
|
|
|
|
function openWhatsappModal() {
|
|
state.whatsappOpen = true;
|
|
state.whatsappDraft = state.whatsapp || "";
|
|
state.whatsappStatusText = "";
|
|
state.whatsappStatusType = "";
|
|
renderWhatsappModal();
|
|
window.setTimeout(() => document.querySelector("#whatsappInput")?.focus(), 60);
|
|
}
|
|
|
|
function closeWhatsappModal() {
|
|
if (state.whatsappSaving) return;
|
|
state.whatsappOpen = false;
|
|
state.whatsappStatusText = "";
|
|
state.whatsappStatusType = "";
|
|
renderWhatsappModal();
|
|
}
|
|
|
|
async function saveWhatsapp(event) {
|
|
if (event) event.preventDefault();
|
|
if (state.whatsappSaving) return;
|
|
|
|
const input = document.querySelector("#whatsappInput");
|
|
const whatsapp = String(input?.value || state.whatsappDraft || "").trim();
|
|
state.whatsappDraft = whatsapp;
|
|
|
|
if (!whatsapp) {
|
|
setWhatsappStatus(message("whatsapp_required"), "error");
|
|
renderWhatsappModal();
|
|
return;
|
|
}
|
|
|
|
if (whatsapp.length > 64) {
|
|
setWhatsappStatus(message("whatsapp_too_long"), "error");
|
|
renderWhatsappModal();
|
|
return;
|
|
}
|
|
|
|
state.whatsappSaving = true;
|
|
setWhatsappStatus(message("saving"));
|
|
renderWhatsappModal();
|
|
|
|
try {
|
|
const profile = await requestGoJSON(endpointSellerWhatsapp(), {
|
|
method: "POST",
|
|
body: { whatsapp }
|
|
}) || {};
|
|
state.sellerProfile = profile;
|
|
state.whatsapp = pickWhatsapp(profile) || whatsapp;
|
|
state.whatsappOpen = false;
|
|
state.whatsappStatusText = "";
|
|
state.whatsappStatusType = "";
|
|
showToast(message("whatsapp_saved"));
|
|
} catch (error) {
|
|
console.error("Save WhatsApp failed:", error);
|
|
const text = error.response?.errorMsg || error.message || message("failed_to_load");
|
|
state.whatsappStatusText = text;
|
|
state.whatsappStatusType = "error";
|
|
showToast(text);
|
|
} finally {
|
|
state.whatsappSaving = false;
|
|
renderAll();
|
|
}
|
|
}
|
|
|
|
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("#coinAmountInput")?.addEventListener("input", (event) => {
|
|
const value = sanitizeCoinAmount(event.target.value);
|
|
state.rechargeAmount = value;
|
|
if (event.target.value !== value) event.target.value = value;
|
|
setRechargeStatus("");
|
|
renderCoinRecharge();
|
|
});
|
|
document.querySelector("#searchForm")?.addEventListener("submit", searchTarget);
|
|
document.querySelector("#coinRechargeForm")?.addEventListener("submit", rechargeCoins);
|
|
document.querySelector("[data-change-target]")?.addEventListener("click", changeTarget);
|
|
document.querySelector("[data-help-open]")?.addEventListener("click", () => setModalOpen("#helpModal", true));
|
|
document.querySelector("[data-history-open]")?.addEventListener("click", () => openHistoryModal());
|
|
document.querySelector("[data-whatsapp-open]")?.addEventListener("click", openWhatsappModal);
|
|
document.querySelector("#whatsappInput")?.addEventListener("input", (event) => {
|
|
state.whatsappDraft = String(event.target.value || "");
|
|
if (state.whatsappStatusText) setWhatsappStatus("");
|
|
renderWhatsappModal();
|
|
});
|
|
document.querySelector("#whatsappForm")?.addEventListener("submit", saveWhatsapp);
|
|
document.querySelector("#historySearchInput")?.addEventListener("input", (event) => {
|
|
if (!String(event.target.value || "").trim()) state.historySearch = "";
|
|
});
|
|
document.querySelector("#historySearchForm")?.addEventListener("submit", searchHistory);
|
|
document.querySelector("#historyClearButton")?.addEventListener("click", () => clearHistorySearch());
|
|
document.querySelector("#historyLoadMore")?.addEventListener("click", () => fetchHistoryRecords({ append: true }));
|
|
document.querySelectorAll("[data-history-tab]").forEach((button) => {
|
|
button.addEventListener("click", () => switchHistoryTab(button.dataset.historyTab));
|
|
});
|
|
document.querySelectorAll("[data-help-close]").forEach((node) => {
|
|
node.addEventListener("click", () => setModalOpen("#helpModal", false));
|
|
});
|
|
document.querySelectorAll("[data-history-close]").forEach((node) => {
|
|
node.addEventListener("click", closeHistoryModal);
|
|
});
|
|
document.querySelectorAll("[data-whatsapp-close]").forEach((node) => {
|
|
node.addEventListener("click", closeWhatsappModal);
|
|
});
|
|
document.addEventListener("keydown", (event) => {
|
|
if (event.key === "Escape") {
|
|
setModalOpen("#helpModal", false);
|
|
closeHistoryModal();
|
|
closeWhatsappModal();
|
|
}
|
|
});
|
|
}
|
|
|
|
async function initializePage() {
|
|
setLoading(true);
|
|
bindEvents();
|
|
applyLanguage(currentLang);
|
|
renderAll();
|
|
|
|
if (!readRawParam("token")) {
|
|
setSearchStatus(message("missing_token"), "error");
|
|
renderAll();
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
const initialResults = await Promise.allSettled([
|
|
fetchSelfProfile(),
|
|
fetchCoinBalance(),
|
|
fetchSellerProfile()
|
|
]);
|
|
initialResults.forEach((result) => {
|
|
if (result.status === "rejected") {
|
|
console.warn("Recharge center initial request failed:", result.reason);
|
|
}
|
|
});
|
|
renderAll();
|
|
setLoading(false);
|
|
|
|
if (state.query) {
|
|
await searchTarget();
|
|
}
|
|
}
|
|
|
|
initializePage();
|
|
})();
|