822 lines
28 KiB
JavaScript
822 lines
28 KiB
JavaScript
(function () {
|
|
const API_BASE_URL = "https://jvapi.haiyihy.com";
|
|
const EXCHANGE_RATE = 80000;
|
|
const supportedLanguages = ["en", "ar", "tr", "id"];
|
|
const languageLabels = {
|
|
en: "EN",
|
|
ar: "AR",
|
|
tr: "TR",
|
|
id: "ID"
|
|
};
|
|
|
|
const translations = {
|
|
en: {
|
|
page_label: "Withdraw and exchange",
|
|
back: "Back",
|
|
title: "Withdraw/Exchange",
|
|
language_button_aria: "Change language",
|
|
available_income: "salary",
|
|
withdraw: "Withdraw",
|
|
exchange: "Exchange",
|
|
transfer: "Transfer",
|
|
withdraw_amount: "Withdraw amount",
|
|
exchange_amount: "Exchange amount",
|
|
transfer_amount: "Transfer amount",
|
|
transfer_to: "Transfer to",
|
|
search: "Search",
|
|
searching: "Searching...",
|
|
minimum: "Minimum 50 USD",
|
|
all: "All",
|
|
exchange_rate: "Exchange rate",
|
|
payment_method: "USDT TRC20 address",
|
|
payment_method_subtitle: "Withdrawals are sent to your TRC20 USDT address.",
|
|
no_payment_info: "No USDT address",
|
|
no_payment_info_copy: "Fill in a TRC20 USDT address before withdrawing.",
|
|
trc20_address: "TRC20 address",
|
|
save_address: "Save address",
|
|
saving: "Saving...",
|
|
usdt_only: "USDT only",
|
|
withdraw_now: "Withdraw now",
|
|
exchange_now: "Exchange now",
|
|
transfer_now: "Transfer now",
|
|
submitting: "Submitting...",
|
|
payee: "Payee",
|
|
uid_prefix: "UID:",
|
|
missing_token: "Missing token",
|
|
load_balance_failed: "Failed to load balance",
|
|
load_payment_failed: "Failed to load payment info",
|
|
missing_payment_info: "Please add a TRC20 USDT address",
|
|
invalid_address: "Please enter a valid TRC20 USDT address",
|
|
invalid_amount: "Please enter a valid amount",
|
|
amount_too_low: "Minimum withdrawal amount is 50 USD",
|
|
amount_exceeds_balance: "Amount exceeds salary",
|
|
payee_not_found: "User not found",
|
|
missing_payee: "Please search and select a user",
|
|
address_saved: "Address saved",
|
|
withdraw_success: "Withdrawal submitted",
|
|
withdraw_failed: "Withdrawal failed",
|
|
exchange_success: "Exchange success",
|
|
exchange_failed: "Exchange failed",
|
|
transfer_success: "Transfer success",
|
|
transfer_failed: "Transfer failed"
|
|
},
|
|
ar: {
|
|
page_label: "السحب والتبديل",
|
|
back: "رجوع",
|
|
title: "سحب/تبديل",
|
|
language_button_aria: "تغيير اللغة",
|
|
available_income: "الراتب",
|
|
withdraw: "سحب",
|
|
exchange: "تبديل",
|
|
transfer: "تحويل",
|
|
withdraw_amount: "مبلغ السحب",
|
|
exchange_amount: "مبلغ التبديل",
|
|
transfer_amount: "مبلغ التحويل",
|
|
transfer_to: "تحويل إلى",
|
|
search: "بحث",
|
|
searching: "جار البحث...",
|
|
minimum: "الحد الأدنى 50 USD",
|
|
all: "الكل",
|
|
exchange_rate: "سعر التبديل",
|
|
payment_method: "عنوان USDT TRC20",
|
|
payment_method_subtitle: "يتم إرسال السحب إلى عنوان USDT TRC20.",
|
|
no_payment_info: "لا يوجد عنوان USDT",
|
|
no_payment_info_copy: "أدخل عنوان USDT TRC20 قبل السحب.",
|
|
trc20_address: "عنوان TRC20",
|
|
save_address: "حفظ العنوان",
|
|
saving: "جار الحفظ...",
|
|
usdt_only: "USDT فقط",
|
|
withdraw_now: "اسحب الآن",
|
|
exchange_now: "بدل الآن",
|
|
transfer_now: "حوّل الآن",
|
|
submitting: "جار الإرسال...",
|
|
payee: "المستفيد",
|
|
uid_prefix: "UID:",
|
|
missing_token: "رمز الدخول مفقود",
|
|
load_balance_failed: "فشل تحميل الرصيد",
|
|
load_payment_failed: "فشل تحميل معلومات الدفع",
|
|
missing_payment_info: "يرجى إضافة عنوان USDT TRC20",
|
|
invalid_address: "يرجى إدخال عنوان USDT TRC20 صالح",
|
|
invalid_amount: "يرجى إدخال مبلغ صالح",
|
|
amount_too_low: "الحد الأدنى للسحب هو 50 USD",
|
|
amount_exceeds_balance: "المبلغ يتجاوز الراتب",
|
|
payee_not_found: "لم يتم العثور على المستخدم",
|
|
missing_payee: "يرجى البحث واختيار مستخدم",
|
|
address_saved: "تم حفظ العنوان",
|
|
withdraw_success: "تم إرسال طلب السحب",
|
|
withdraw_failed: "فشل السحب",
|
|
exchange_success: "تم التبديل",
|
|
exchange_failed: "فشل التبديل",
|
|
transfer_success: "تم التحويل",
|
|
transfer_failed: "فشل التحويل"
|
|
},
|
|
tr: {
|
|
page_label: "Çekme ve değişim",
|
|
back: "Geri",
|
|
title: "Çek/Değiştir",
|
|
language_button_aria: "Dili değiştir",
|
|
available_income: "Maaş",
|
|
withdraw: "Withdraw",
|
|
exchange: "Exchange",
|
|
transfer: "Transfer",
|
|
withdraw_amount: "Withdraw amount",
|
|
exchange_amount: "Exchange amount",
|
|
transfer_amount: "Transfer amount",
|
|
transfer_to: "Transfer to",
|
|
search: "Search",
|
|
searching: "Searching...",
|
|
minimum: "Minimum 50 USD",
|
|
all: "All",
|
|
exchange_rate: "Exchange rate",
|
|
payment_method: "USDT TRC20 address",
|
|
payment_method_subtitle: "Withdrawals are sent to your TRC20 USDT address.",
|
|
no_payment_info: "No USDT address",
|
|
no_payment_info_copy: "Fill in a TRC20 USDT address before withdrawing.",
|
|
trc20_address: "TRC20 address",
|
|
save_address: "Save address",
|
|
saving: "Saving...",
|
|
usdt_only: "USDT only",
|
|
withdraw_now: "Withdraw now",
|
|
exchange_now: "Exchange now",
|
|
transfer_now: "Transfer now",
|
|
submitting: "Submitting...",
|
|
payee: "Payee",
|
|
uid_prefix: "UID:",
|
|
missing_token: "Missing token",
|
|
load_balance_failed: "Failed to load balance",
|
|
load_payment_failed: "Failed to load payment info",
|
|
missing_payment_info: "Please add a TRC20 USDT address",
|
|
invalid_address: "Please enter a valid TRC20 USDT address",
|
|
invalid_amount: "Please enter a valid amount",
|
|
amount_too_low: "Minimum withdrawal amount is 50 USD",
|
|
amount_exceeds_balance: "Amount exceeds salary",
|
|
payee_not_found: "User not found",
|
|
missing_payee: "Please search and select a user",
|
|
address_saved: "Address saved",
|
|
withdraw_success: "Withdrawal submitted",
|
|
withdraw_failed: "Withdrawal failed",
|
|
exchange_success: "Exchange success",
|
|
exchange_failed: "Exchange failed",
|
|
transfer_success: "Transfer success",
|
|
transfer_failed: "Transfer failed"
|
|
},
|
|
id: {
|
|
page_label: "Tarik dan tukar",
|
|
back: "Kembali",
|
|
title: "Tarik/Tukar",
|
|
language_button_aria: "Ubah bahasa",
|
|
available_income: "Gaji",
|
|
withdraw: "Tarik",
|
|
exchange: "Tukar",
|
|
transfer: "Transfer",
|
|
withdraw_amount: "Jumlah penarikan",
|
|
exchange_amount: "Jumlah penukaran",
|
|
transfer_amount: "Jumlah transfer",
|
|
transfer_to: "Transfer ke",
|
|
search: "Cari",
|
|
searching: "Mencari...",
|
|
minimum: "Minimum 50 USD",
|
|
all: "Semua",
|
|
exchange_rate: "Kurs penukaran",
|
|
payment_method: "Alamat USDT TRC20",
|
|
payment_method_subtitle: "Penarikan dikirim ke alamat USDT TRC20 Anda.",
|
|
no_payment_info: "Tidak ada alamat USDT",
|
|
no_payment_info_copy: "Isi alamat USDT TRC20 sebelum menarik.",
|
|
trc20_address: "Alamat TRC20",
|
|
save_address: "Simpan alamat",
|
|
saving: "Menyimpan...",
|
|
usdt_only: "Hanya USDT",
|
|
withdraw_now: "Tarik sekarang",
|
|
exchange_now: "Tukar sekarang",
|
|
transfer_now: "Transfer sekarang",
|
|
submitting: "Mengirim...",
|
|
payee: "Penerima",
|
|
uid_prefix: "UID:",
|
|
missing_token: "Token tidak ditemukan",
|
|
load_balance_failed: "Gagal memuat saldo",
|
|
load_payment_failed: "Gagal memuat informasi pembayaran",
|
|
missing_payment_info: "Tambahkan alamat USDT TRC20",
|
|
invalid_address: "Masukkan alamat USDT TRC20 yang valid",
|
|
invalid_amount: "Masukkan jumlah yang valid",
|
|
amount_too_low: "Minimum penarikan adalah 50 USD",
|
|
amount_exceeds_balance: "Jumlah melebihi gaji",
|
|
payee_not_found: "Pengguna tidak ditemukan",
|
|
missing_payee: "Cari dan pilih pengguna",
|
|
address_saved: "Alamat disimpan",
|
|
withdraw_success: "Penarikan terkirim",
|
|
withdraw_failed: "Penarikan gagal",
|
|
exchange_success: "Penukaran berhasil",
|
|
exchange_failed: "Penukaran gagal",
|
|
transfer_success: "Transfer berhasil",
|
|
transfer_failed: "Transfer gagal"
|
|
}
|
|
};
|
|
|
|
const params = readURLParams();
|
|
let currentLang = normalizeLanguage(params.get("lang"));
|
|
let currentMode = normalizeMode(params.get("mode")) || "withdraw";
|
|
let currentMessages = translations[currentLang] || translations.en;
|
|
let availableBalance = 0;
|
|
let selectedPaymentInfo = null;
|
|
let selectedPayee = null;
|
|
let currentProfile = null;
|
|
let isSubmitting = false;
|
|
let isSavingAddress = false;
|
|
let isSearchingPayee = false;
|
|
|
|
const inputs = {
|
|
withdraw: document.querySelector("#withdrawAmount"),
|
|
exchange: document.querySelector("#exchangeAmount"),
|
|
transfer: document.querySelector("#transferAmount")
|
|
};
|
|
const balanceAmount = document.querySelector("#balanceAmount");
|
|
const emptyState = document.querySelector(".empty-address-state");
|
|
const addressForm = document.querySelector("#addressForm");
|
|
const trc20AddressInput = document.querySelector("#trc20Address");
|
|
const saveAddressButton = document.querySelector("#saveAddressButton");
|
|
const boundCard = document.querySelector(".bound-address-card");
|
|
const paymentSection = document.querySelector("[data-payment-section]");
|
|
const submitButton = document.querySelector(".submit-button");
|
|
const toast = document.querySelector(".toast");
|
|
const payeeResult = document.querySelector("#payeeResult");
|
|
const transferAccountInput = document.querySelector("#transferAccount");
|
|
const searchPayeeButton = document.querySelector("#searchPayeeButton");
|
|
const exchangeRateText = document.querySelector("#exchangeRateText");
|
|
|
|
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();
|
|
return supportedLanguages.includes(value) ? value : "en";
|
|
}
|
|
|
|
function normalizeMode(mode) {
|
|
return ["withdraw", "exchange", "transfer"].includes(mode) ? mode : "";
|
|
}
|
|
|
|
function apiBaseURL() {
|
|
const override = window.HYAPP_API_BASE_URL || params.get("apiBase");
|
|
if (override) return String(override).replace(/\/+$/, "");
|
|
return API_BASE_URL;
|
|
}
|
|
|
|
function normalizeAuthorization(token) {
|
|
const value = String(token || "").trim();
|
|
if (!value) return "";
|
|
return /^bearer\s+/i.test(value) ? value : `Bearer ${value}`;
|
|
}
|
|
|
|
function authHeadersFromToken(token, hasBody) {
|
|
const headers = {
|
|
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"
|
|
};
|
|
if (hasBody) headers["content-type"] = "application/json";
|
|
return headers;
|
|
}
|
|
|
|
async function requestJSON(path, options = {}) {
|
|
const token = readRawParam("token");
|
|
if (!token) throw new Error(message("missing_token", "Missing token"));
|
|
|
|
const response = await fetch(`${apiBaseURL()}${path}`, {
|
|
method: options.method || "GET",
|
|
cache: "no-store",
|
|
headers: authHeadersFromToken(token, options.body !== undefined),
|
|
body: options.body === undefined ? undefined : JSON.stringify(options.body)
|
|
});
|
|
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;
|
|
}
|
|
|
|
function message(key, fallback) {
|
|
return currentMessages[key] || translations.en[key] || fallback || key;
|
|
}
|
|
|
|
function formatMoney(value) {
|
|
const number = Number(value);
|
|
return Number.isFinite(number) ? number.toFixed(2) : "0.00";
|
|
}
|
|
|
|
function moneyNumber(value) {
|
|
const number = Number(value);
|
|
return Number.isFinite(number) ? number : 0;
|
|
}
|
|
|
|
function activeInput() {
|
|
return inputs[currentMode] || null;
|
|
}
|
|
|
|
function activeAmount() {
|
|
return moneyNumber(activeInput()?.value);
|
|
}
|
|
|
|
function formatExchangeRate() {
|
|
return `1$ = ${EXCHANGE_RATE} golds`;
|
|
}
|
|
|
|
function isValidTrc20Address(value) {
|
|
return /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(String(value || "").trim());
|
|
}
|
|
|
|
function maskAddress(value) {
|
|
const text = String(value || "").trim();
|
|
if (text.length <= 12) return text || "-";
|
|
return `${text.slice(0, 6)}...${text.slice(-6)}`;
|
|
}
|
|
|
|
function applyLanguage(lang) {
|
|
currentLang = normalizeLanguage(lang);
|
|
currentMessages = translations[currentLang] || translations.en;
|
|
|
|
document.documentElement.lang = currentLang;
|
|
document.documentElement.dir = currentLang === "ar" ? "rtl" : "ltr";
|
|
document.querySelectorAll("[data-i18n]").forEach((node) => {
|
|
const value = currentMessages[node.dataset.i18n];
|
|
if (value) node.textContent = value;
|
|
});
|
|
document.querySelectorAll("[data-i18n-aria]").forEach((node) => {
|
|
const value = currentMessages[node.dataset.i18nAria];
|
|
if (value) node.setAttribute("aria-label", value);
|
|
});
|
|
|
|
const languageButton = document.querySelector(".language-button");
|
|
if (languageButton) {
|
|
languageButton.textContent = languageLabels[currentLang];
|
|
languageButton.setAttribute("aria-label", message("language_button_aria", "Change language"));
|
|
}
|
|
document.querySelectorAll(".language-menu [data-lang]").forEach((button) => {
|
|
const isActive = button.dataset.lang === currentLang;
|
|
button.classList.toggle("is-active", isActive);
|
|
button.setAttribute("aria-pressed", String(isActive));
|
|
});
|
|
if (exchangeRateText) exchangeRateText.textContent = formatExchangeRate();
|
|
renderPayee();
|
|
updateSubmitState();
|
|
}
|
|
|
|
function setMode(mode) {
|
|
const nextMode = normalizeMode(mode) || "withdraw";
|
|
currentMode = nextMode;
|
|
document.querySelectorAll(".mode-tab").forEach((tab) => {
|
|
const active = tab.dataset.mode === nextMode;
|
|
tab.classList.toggle("active", active);
|
|
tab.setAttribute("aria-selected", String(active));
|
|
});
|
|
document.querySelectorAll(".mode-panel").forEach((panel) => {
|
|
panel.classList.toggle("active", panel.dataset.panel === nextMode);
|
|
});
|
|
if (paymentSection) paymentSection.hidden = nextMode !== "withdraw";
|
|
updateSubmitState();
|
|
}
|
|
|
|
function updateSubmitState() {
|
|
if (!submitButton) return;
|
|
if (isSubmitting) {
|
|
submitButton.textContent = message("submitting", "Submitting...");
|
|
submitButton.disabled = true;
|
|
return;
|
|
}
|
|
|
|
const submitLabels = {
|
|
withdraw: message("withdraw_now", "Withdraw now"),
|
|
exchange: message("exchange_now", "Exchange now"),
|
|
transfer: message("transfer_now", "Transfer now")
|
|
};
|
|
submitButton.textContent = submitLabels[currentMode];
|
|
|
|
const amount = activeAmount();
|
|
const exceedsBalance = amount > availableBalance;
|
|
const missingWithdrawInfo = currentMode === "withdraw" && !selectedPaymentInfo?.id;
|
|
const missingPayee = currentMode === "transfer" && !selectedPayee?.id;
|
|
|
|
submitButton.disabled = !amount ||
|
|
amount <= 0 ||
|
|
exceedsBalance ||
|
|
missingWithdrawInfo ||
|
|
missingPayee ||
|
|
(currentMode === "withdraw" && amount < 50);
|
|
}
|
|
|
|
function showToast(keyOrText, fallback) {
|
|
if (!toast) return;
|
|
toast.textContent = message(keyOrText, fallback || keyOrText);
|
|
toast.hidden = false;
|
|
window.clearTimeout(showToast.timer);
|
|
showToast.timer = window.setTimeout(() => {
|
|
toast.hidden = true;
|
|
}, 2200);
|
|
}
|
|
|
|
function renderBalance(value) {
|
|
availableBalance = moneyNumber(value);
|
|
if (balanceAmount) balanceAmount.textContent = formatMoney(availableBalance);
|
|
Object.values(inputs).forEach((input) => {
|
|
if (input) input.max = formatMoney(availableBalance);
|
|
});
|
|
updateSubmitState();
|
|
}
|
|
|
|
function renderPaymentInfo(paymentInfo) {
|
|
selectedPaymentInfo = paymentInfo || null;
|
|
|
|
if (!selectedPaymentInfo) {
|
|
if (emptyState) emptyState.hidden = false;
|
|
if (addressForm) addressForm.hidden = false;
|
|
if (boundCard) boundCard.hidden = true;
|
|
updateSubmitState();
|
|
return;
|
|
}
|
|
|
|
const badge = boundCard?.querySelector(".network-badge");
|
|
const network = boundCard?.querySelector(".bound-network");
|
|
const address = boundCard?.querySelector(".bound-address");
|
|
const meta = boundCard?.querySelector(".bound-meta");
|
|
if (badge) badge.textContent = "TRC20";
|
|
if (network) network.textContent = "USDT TRC20";
|
|
if (address) address.textContent = maskAddress(selectedPaymentInfo.cardNo);
|
|
if (meta) meta.textContent = message("usdt_only", "USDT only");
|
|
|
|
if (emptyState) emptyState.hidden = true;
|
|
if (addressForm) addressForm.hidden = true;
|
|
if (boundCard) boundCard.hidden = false;
|
|
updateSubmitState();
|
|
}
|
|
|
|
function renderPayee() {
|
|
if (!payeeResult) return;
|
|
|
|
if (!selectedPayee) {
|
|
payeeResult.hidden = true;
|
|
payeeResult.innerHTML = "";
|
|
updateSubmitState();
|
|
return;
|
|
}
|
|
|
|
const name = selectedPayee.userNickname || selectedPayee.name || "-";
|
|
const account = selectedPayee.ownSpecialId?.account || selectedPayee.account || selectedPayee.id || "-";
|
|
const avatar = selectedPayee.userAvatar || selectedPayee.avatar || "";
|
|
payeeResult.hidden = false;
|
|
payeeResult.innerHTML = `
|
|
<div class="payee-avatar">${avatar ? `<img src="${escapeHTML(avatar)}" alt="">` : escapeHTML(name.slice(0, 1) || "U")}</div>
|
|
<div class="payee-copy">
|
|
<div class="payee-name">${escapeHTML(name)}</div>
|
|
<div class="payee-id">${escapeHTML(message("uid_prefix", "UID:"))} ${escapeHTML(account)}</div>
|
|
</div>
|
|
`;
|
|
updateSubmitState();
|
|
}
|
|
|
|
function escapeHTML(value) {
|
|
return String(value || "").replace(/[&<>"']/g, (char) => ({
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
'"': """,
|
|
"'": "'"
|
|
})[char]);
|
|
}
|
|
|
|
async function fetchBalance() {
|
|
try {
|
|
const data = await requestJSON("/wallet/bank/balance");
|
|
const body = data.body ?? data.data ?? 0;
|
|
renderBalance(typeof body === "number" ? body : body.currentSalary ?? body.availableBalance ?? body.balance ?? 0);
|
|
} catch (error) {
|
|
console.error("Failed to load bank balance:", error);
|
|
renderBalance(0);
|
|
showToast("load_balance_failed", error.message);
|
|
}
|
|
}
|
|
|
|
async function fetchUserProfile() {
|
|
try {
|
|
const data = await requestJSON("/team/member/profile");
|
|
currentProfile = data.body ?? data.data ?? null;
|
|
} catch (error) {
|
|
console.error("Failed to load user profile:", error);
|
|
currentProfile = null;
|
|
}
|
|
}
|
|
|
|
async function fetchPaymentInfo() {
|
|
try {
|
|
const data = await requestJSON("/wallet/bank-card/list");
|
|
const cards = Array.isArray(data.body) ? data.body : Array.isArray(data.data) ? data.data : [];
|
|
const usdtCards = cards.filter((item) => item.cardType === "USDT");
|
|
renderPaymentInfo(usdtCards.find((item) => item.use) || usdtCards[0] || null);
|
|
} catch (error) {
|
|
console.error("Failed to load payment info:", error);
|
|
renderPaymentInfo(null);
|
|
showToast("load_payment_failed", error.message);
|
|
}
|
|
}
|
|
|
|
function normalizeAmountInput(event) {
|
|
const input = event?.target || activeInput();
|
|
if (!input) return;
|
|
const raw = String(input.value || "").replace(/[^\d.]/g, "");
|
|
const parts = raw.split(".");
|
|
let nextValue = parts.length > 2 ? `${parts[0]}.${parts.slice(1).join("")}` : raw;
|
|
const amount = Number(nextValue);
|
|
if (Number.isFinite(amount) && amount > availableBalance) nextValue = formatMoney(availableBalance);
|
|
input.value = nextValue;
|
|
updateSubmitState();
|
|
}
|
|
|
|
function validateAmount(requireMinimum) {
|
|
const amount = activeAmount();
|
|
if (!amount || amount <= 0) return "invalid_amount";
|
|
if (requireMinimum && amount < 50) return "amount_too_low";
|
|
if (amount > availableBalance) return "amount_exceeds_balance";
|
|
return "";
|
|
}
|
|
|
|
async function saveAddress() {
|
|
const address = String(trc20AddressInput?.value || "").trim();
|
|
if (!isValidTrc20Address(address)) {
|
|
showToast("invalid_address");
|
|
return;
|
|
}
|
|
if (!currentProfile) await fetchUserProfile();
|
|
|
|
isSavingAddress = true;
|
|
if (saveAddressButton) {
|
|
saveAddressButton.disabled = true;
|
|
saveAddressButton.textContent = message("saving", "Saving...");
|
|
}
|
|
|
|
try {
|
|
const data = await requestJSON("/wallet/bank-card/add", {
|
|
method: "POST",
|
|
body: {
|
|
userId: currentProfile?.id || currentProfile?.userId || "",
|
|
cardType: "USDT",
|
|
cardName: "USDT",
|
|
cardNo: address,
|
|
payee: "USDT-TRC20"
|
|
}
|
|
});
|
|
const created = data.body || data.data || null;
|
|
if (created?.id) {
|
|
renderPaymentInfo({ ...created, cardType: "USDT", cardNo: address });
|
|
await requestJSON("/wallet/bank-card/use", {
|
|
method: "POST",
|
|
body: { id: created.id }
|
|
}).catch((error) => console.warn("Failed to set USDT address in use:", error));
|
|
}
|
|
await fetchPaymentInfo();
|
|
showToast("address_saved");
|
|
} catch (error) {
|
|
console.error("Failed to save TRC20 address:", error);
|
|
showToast(error.message || message("load_payment_failed", "Failed to load payment info"));
|
|
} finally {
|
|
isSavingAddress = false;
|
|
if (saveAddressButton) {
|
|
saveAddressButton.disabled = false;
|
|
saveAddressButton.textContent = message("save_address", "Save address");
|
|
}
|
|
}
|
|
}
|
|
|
|
async function searchPayee() {
|
|
const account = String(transferAccountInput?.value || "").trim();
|
|
if (!account) {
|
|
showToast("missing_payee");
|
|
return;
|
|
}
|
|
|
|
selectedPayee = null;
|
|
renderPayee();
|
|
isSearchingPayee = true;
|
|
if (searchPayeeButton) {
|
|
searchPayeeButton.disabled = true;
|
|
searchPayeeButton.textContent = message("searching", "Searching...");
|
|
}
|
|
|
|
try {
|
|
const path = `/wallet/bank/search/user-profile?account=${encodeURIComponent(account)}&type=AGENT&searchType=TRANSFER`;
|
|
const data = await requestJSON(path);
|
|
const body = data.body ?? data.data ?? null;
|
|
if (!body?.id) {
|
|
showToast("payee_not_found");
|
|
return;
|
|
}
|
|
selectedPayee = body;
|
|
renderPayee();
|
|
} catch (error) {
|
|
console.error("Failed to search payee:", error);
|
|
showToast(error.message || message("payee_not_found", "User not found"));
|
|
} finally {
|
|
isSearchingPayee = false;
|
|
if (searchPayeeButton) {
|
|
searchPayeeButton.disabled = false;
|
|
searchPayeeButton.textContent = message("search", "Search");
|
|
}
|
|
}
|
|
}
|
|
|
|
function validateWithdraw() {
|
|
if (!selectedPaymentInfo?.id) return "missing_payment_info";
|
|
return validateAmount(true);
|
|
}
|
|
|
|
function validateTransfer() {
|
|
if (!selectedPayee?.id) return "missing_payee";
|
|
return validateAmount(false);
|
|
}
|
|
|
|
async function submitWithdraw() {
|
|
const errorKey = validateWithdraw();
|
|
if (errorKey) {
|
|
showToast(errorKey);
|
|
return;
|
|
}
|
|
|
|
await requestJSON("/wallet/bank/withdraw", {
|
|
method: "POST",
|
|
body: {
|
|
amount: activeAmount(),
|
|
acceptBankCardId: selectedPaymentInfo.id,
|
|
salaryType: "HOST_SALARY"
|
|
}
|
|
});
|
|
showToast("withdraw_success");
|
|
}
|
|
|
|
async function submitExchange() {
|
|
const errorKey = validateAmount(false);
|
|
if (errorKey) {
|
|
showToast(errorKey);
|
|
return;
|
|
}
|
|
|
|
await requestJSON("/wallet/bank/exchange/gold", {
|
|
method: "POST",
|
|
body: {
|
|
amount: activeAmount()
|
|
}
|
|
});
|
|
showToast("exchange_success");
|
|
}
|
|
|
|
async function submitTransfer() {
|
|
const errorKey = validateTransfer();
|
|
if (errorKey) {
|
|
showToast(errorKey);
|
|
return;
|
|
}
|
|
|
|
await requestJSON("/wallet/bank/transferGold", {
|
|
method: "POST",
|
|
body: {
|
|
amount: activeAmount(),
|
|
acceptUserId: selectedPayee.id,
|
|
acceptMethod: "BANK_TRANSFER"
|
|
}
|
|
});
|
|
showToast("transfer_success");
|
|
}
|
|
|
|
async function submitCurrentMode() {
|
|
if (isSubmitting) return;
|
|
|
|
isSubmitting = true;
|
|
updateSubmitState();
|
|
try {
|
|
if (currentMode === "withdraw") await submitWithdraw();
|
|
if (currentMode === "exchange") await submitExchange();
|
|
if (currentMode === "transfer") await submitTransfer();
|
|
const input = activeInput();
|
|
if (input) input.value = "";
|
|
await fetchBalance();
|
|
} catch (error) {
|
|
console.error(`Failed to submit ${currentMode}:`, error);
|
|
const fallback = {
|
|
withdraw: "withdraw_failed",
|
|
exchange: "exchange_failed",
|
|
transfer: "transfer_failed"
|
|
}[currentMode];
|
|
showToast(error.message || message(fallback, "Submit failed"));
|
|
} finally {
|
|
isSubmitting = false;
|
|
updateSubmitState();
|
|
}
|
|
}
|
|
|
|
function syncBackFallback() {
|
|
document.querySelector(".back-button")?.addEventListener("click", () => {
|
|
if (window.history.length > 1) {
|
|
window.history.back();
|
|
return;
|
|
}
|
|
const nextParams = new URLSearchParams(params);
|
|
const token = readRawParam("token");
|
|
if (token) nextParams.set("token", token);
|
|
if (currentLang) nextParams.set("lang", currentLang);
|
|
const query = nextParams.toString();
|
|
window.location.href = `../host-center/index.html${query ? `?${query}` : ""}`;
|
|
});
|
|
}
|
|
|
|
function setLanguageMenuOpen(open) {
|
|
const languageButton = document.querySelector(".language-button");
|
|
const languageMenu = document.querySelector(".language-menu");
|
|
if (!languageButton || !languageMenu) return;
|
|
|
|
languageMenu.hidden = !open;
|
|
languageButton.setAttribute("aria-expanded", String(open));
|
|
}
|
|
|
|
document.querySelector(".language-button")?.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
const languageMenu = document.querySelector(".language-menu");
|
|
setLanguageMenuOpen(Boolean(languageMenu?.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.querySelectorAll(".mode-tab").forEach((tab) => {
|
|
tab.addEventListener("click", () => setMode(tab.dataset.mode));
|
|
});
|
|
|
|
document.querySelector(".text-action")?.addEventListener("click", () => {
|
|
if (inputs.withdraw) inputs.withdraw.value = formatMoney(availableBalance);
|
|
updateSubmitState();
|
|
});
|
|
|
|
Object.values(inputs).forEach((input) => input?.addEventListener("input", normalizeAmountInput));
|
|
transferAccountInput?.addEventListener("input", () => {
|
|
selectedPayee = null;
|
|
renderPayee();
|
|
});
|
|
transferAccountInput?.addEventListener("keydown", (event) => {
|
|
if (event.key === "Enter") searchPayee();
|
|
});
|
|
searchPayeeButton?.addEventListener("click", searchPayee);
|
|
saveAddressButton?.addEventListener("click", saveAddress);
|
|
submitButton?.addEventListener("click", submitCurrentMode);
|
|
|
|
syncBackFallback();
|
|
applyLanguage(currentLang);
|
|
setMode(currentMode);
|
|
fetchUserProfile();
|
|
fetchBalance();
|
|
fetchPaymentInfo();
|
|
})();
|