516 lines
18 KiB
JavaScript
516 lines
18 KiB
JavaScript
(function () {
|
||
const API_BASE_URL = "https://jvapi.haiyihy.com";
|
||
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: "Available Income",
|
||
withdraw: "Withdraw",
|
||
exchange: "Exchange",
|
||
withdraw_amount: "Withdraw amount",
|
||
exchange_amount: "Exchange amount",
|
||
minimum: "Minimum 50 USD",
|
||
all: "All",
|
||
you_receive: "You receive",
|
||
payment_method: "Payment method",
|
||
payment_method_subtitle: "The currently used bank card will receive this withdrawal.",
|
||
manage: "Manage",
|
||
no_bank_card: "No bank card in use",
|
||
no_bank_card_copy: "Set a bank card as in use before withdrawing.",
|
||
change: "Change",
|
||
withdraw_now: "Withdraw now",
|
||
exchange_now: "Exchange now",
|
||
submitting: "Submitting...",
|
||
payee: "Payee",
|
||
card_number: "Card",
|
||
missing_token: "Missing token",
|
||
load_balance_failed: "Failed to load balance",
|
||
load_bank_card_failed: "Failed to load bank card",
|
||
missing_bank_card: "No bank card in use",
|
||
invalid_amount: "Please enter a valid amount",
|
||
amount_too_low: "Minimum withdrawal amount is 50 USD",
|
||
amount_exceeds_balance: "Amount exceeds available income",
|
||
withdraw_success: "Withdrawal submitted",
|
||
withdraw_failed: "Withdrawal failed",
|
||
exchange_unavailable: "Exchange is not available on this page"
|
||
},
|
||
ar: {
|
||
page_label: "السحب والتبديل",
|
||
back: "رجوع",
|
||
title: "سحب/تبديل",
|
||
language_button_aria: "تغيير اللغة",
|
||
available_income: "الدخل المتاح",
|
||
withdraw: "سحب",
|
||
exchange: "تبديل",
|
||
withdraw_amount: "مبلغ السحب",
|
||
exchange_amount: "مبلغ التبديل",
|
||
minimum: "الحد الأدنى 50 USD",
|
||
all: "الكل",
|
||
you_receive: "ستحصل على",
|
||
payment_method: "طريقة الدفع",
|
||
payment_method_subtitle: "سيتم إرسال السحب إلى البطاقة المستخدمة حاليًا.",
|
||
manage: "إدارة",
|
||
no_bank_card: "لا توجد بطاقة مستخدمة",
|
||
no_bank_card_copy: "عيّن بطاقة بنكية مستخدمة قبل السحب.",
|
||
change: "تغيير",
|
||
withdraw_now: "اسحب الآن",
|
||
exchange_now: "بدل الآن",
|
||
submitting: "جار الإرسال...",
|
||
payee: "المستفيد",
|
||
card_number: "البطاقة",
|
||
missing_token: "رمز الدخول مفقود",
|
||
load_balance_failed: "فشل تحميل الرصيد",
|
||
load_bank_card_failed: "فشل تحميل البطاقة",
|
||
missing_bank_card: "لا توجد بطاقة مستخدمة",
|
||
invalid_amount: "يرجى إدخال مبلغ صالح",
|
||
amount_too_low: "الحد الأدنى للسحب هو 50 USD",
|
||
amount_exceeds_balance: "المبلغ يتجاوز الدخل المتاح",
|
||
withdraw_success: "تم إرسال طلب السحب",
|
||
withdraw_failed: "فشل السحب",
|
||
exchange_unavailable: "التبديل غير متاح في هذه الصفحة"
|
||
},
|
||
tr: {
|
||
page_label: "Çekme ve değişim",
|
||
back: "Geri",
|
||
title: "Çek/Değiştir",
|
||
language_button_aria: "Dili değiştir",
|
||
available_income: "Kullanılabilir Gelir",
|
||
withdraw: "Withdraw",
|
||
exchange: "Exchange",
|
||
withdraw_amount: "Withdraw amount",
|
||
exchange_amount: "Exchange amount",
|
||
minimum: "Minimum 50 USD",
|
||
all: "All",
|
||
you_receive: "You receive",
|
||
payment_method: "Payment method",
|
||
payment_method_subtitle: "The currently used bank card will receive this withdrawal.",
|
||
manage: "Manage",
|
||
no_bank_card: "No bank card in use",
|
||
no_bank_card_copy: "Set a bank card as in use before withdrawing.",
|
||
change: "Change",
|
||
withdraw_now: "Withdraw now",
|
||
exchange_now: "Exchange now",
|
||
submitting: "Submitting...",
|
||
payee: "Payee",
|
||
card_number: "Card",
|
||
missing_token: "Missing token",
|
||
load_balance_failed: "Failed to load balance",
|
||
load_bank_card_failed: "Failed to load bank card",
|
||
missing_bank_card: "No bank card in use",
|
||
invalid_amount: "Please enter a valid amount",
|
||
amount_too_low: "Minimum withdrawal amount is 50 USD",
|
||
amount_exceeds_balance: "Amount exceeds available income",
|
||
withdraw_success: "Withdrawal submitted",
|
||
withdraw_failed: "Withdrawal failed",
|
||
exchange_unavailable: "Exchange is not available on this page"
|
||
},
|
||
id: {
|
||
page_label: "Tarik dan tukar",
|
||
back: "Kembali",
|
||
title: "Tarik/Tukar",
|
||
language_button_aria: "Ubah bahasa",
|
||
available_income: "Penghasilan Tersedia",
|
||
withdraw: "Tarik",
|
||
exchange: "Tukar",
|
||
withdraw_amount: "Jumlah penarikan",
|
||
exchange_amount: "Jumlah penukaran",
|
||
minimum: "Minimum 50 USD",
|
||
all: "Semua",
|
||
you_receive: "Anda menerima",
|
||
payment_method: "Metode pembayaran",
|
||
payment_method_subtitle: "Kartu bank yang sedang digunakan akan menerima penarikan ini.",
|
||
manage: "Kelola",
|
||
no_bank_card: "Tidak ada kartu aktif",
|
||
no_bank_card_copy: "Atur kartu bank sebagai aktif sebelum menarik.",
|
||
change: "Ubah",
|
||
withdraw_now: "Tarik sekarang",
|
||
exchange_now: "Tukar sekarang",
|
||
submitting: "Mengirim...",
|
||
payee: "Penerima",
|
||
card_number: "Kartu",
|
||
missing_token: "Token tidak ditemukan",
|
||
load_balance_failed: "Gagal memuat saldo",
|
||
load_bank_card_failed: "Gagal memuat kartu bank",
|
||
missing_bank_card: "Tidak ada kartu aktif",
|
||
invalid_amount: "Masukkan jumlah yang valid",
|
||
amount_too_low: "Minimum penarikan adalah 50 USD",
|
||
amount_exceeds_balance: "Jumlah melebihi penghasilan tersedia",
|
||
withdraw_success: "Penarikan terkirim",
|
||
withdraw_failed: "Penarikan gagal",
|
||
exchange_unavailable: "Penukaran tidak tersedia di halaman ini"
|
||
}
|
||
};
|
||
|
||
const params = readURLParams();
|
||
let currentLang = normalizeLanguage(params.get("lang"));
|
||
let currentMode = "withdraw";
|
||
let currentMessages = translations[currentLang] || translations.en;
|
||
let availableBalance = 0;
|
||
let selectedBankCard = null;
|
||
let isSubmitting = false;
|
||
|
||
const amountInput = document.querySelector("#withdrawAmount");
|
||
const balanceAmount = document.querySelector("#balanceAmount");
|
||
const emptyState = document.querySelector(".empty-address-state");
|
||
const boundCard = document.querySelector(".bound-address-card");
|
||
const submitButton = document.querySelector(".submit-button");
|
||
const toast = document.querySelector(".toast");
|
||
|
||
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 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(currentMessages.missing_token || translations.en.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) {
|
||
throw new Error(data.errorMsg || data.message || `Request failed: ${response.status}`);
|
||
}
|
||
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 maskCardNumber(value) {
|
||
const text = String(value || "").trim();
|
||
if (!text) return "-";
|
||
if (text.length <= 8) return text;
|
||
return `${text.slice(0, 4)} **** ${text.slice(-4)}`;
|
||
}
|
||
|
||
function activeAmount() {
|
||
return moneyNumber(amountInput?.value);
|
||
}
|
||
|
||
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));
|
||
});
|
||
updateSubmitState();
|
||
}
|
||
|
||
function setMode(mode) {
|
||
currentMode = mode;
|
||
document.querySelectorAll(".mode-tab").forEach((tab) => {
|
||
const active = tab.dataset.mode === mode;
|
||
tab.classList.toggle("active", active);
|
||
tab.setAttribute("aria-selected", String(active));
|
||
});
|
||
document.querySelectorAll(".mode-panel").forEach((panel) => {
|
||
panel.classList.toggle("active", panel.dataset.panel === mode);
|
||
});
|
||
updateSubmitState();
|
||
}
|
||
|
||
function updateSubmitState() {
|
||
if (!submitButton) return;
|
||
if (isSubmitting) {
|
||
submitButton.textContent = message("submitting", "Submitting...");
|
||
submitButton.disabled = true;
|
||
return;
|
||
}
|
||
|
||
submitButton.textContent = currentMode === "exchange" ? message("exchange_now", "Exchange now") : message("withdraw_now", "Withdraw now");
|
||
const amount = activeAmount();
|
||
submitButton.disabled = currentMode !== "withdraw" ||
|
||
!selectedBankCard?.id ||
|
||
!amount ||
|
||
amount < 50 ||
|
||
amount > availableBalance;
|
||
}
|
||
|
||
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);
|
||
if (amountInput) amountInput.max = formatMoney(availableBalance);
|
||
updateSubmitState();
|
||
}
|
||
|
||
function renderBankCard(card) {
|
||
selectedBankCard = card || null;
|
||
|
||
if (!selectedBankCard) {
|
||
if (emptyState) emptyState.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 = selectedBankCard.cardType || "BANK";
|
||
if (network) network.textContent = selectedBankCard.cardType || "Bank card";
|
||
if (address) address.textContent = `${message("card_number", "Card")}: ${maskCardNumber(selectedBankCard.cardNo)}`;
|
||
if (meta) meta.textContent = `${message("payee", "Payee")}: ${selectedBankCard.payee || "-"}`;
|
||
|
||
if (emptyState) emptyState.hidden = true;
|
||
if (boundCard) boundCard.hidden = false;
|
||
updateSubmitState();
|
||
}
|
||
|
||
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 fetchBankCards() {
|
||
try {
|
||
const data = await requestJSON("/wallet/bank-card/list");
|
||
const cards = Array.isArray(data.body) ? data.body : Array.isArray(data.data) ? data.data : [];
|
||
renderBankCard(cards.find((item) => item.use));
|
||
} catch (error) {
|
||
console.error("Failed to load bank card:", error);
|
||
renderBankCard(null);
|
||
showToast("load_bank_card_failed", error.message);
|
||
}
|
||
}
|
||
|
||
function normalizeAmountInput() {
|
||
if (!amountInput) return;
|
||
const raw = String(amountInput.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);
|
||
amountInput.value = nextValue;
|
||
updateSubmitState();
|
||
}
|
||
|
||
function validateWithdraw() {
|
||
const amount = activeAmount();
|
||
if (!selectedBankCard?.id) return "missing_bank_card";
|
||
if (!amount || amount <= 0) return "invalid_amount";
|
||
if (amount < 50) return "amount_too_low";
|
||
if (amount > availableBalance) return "amount_exceeds_balance";
|
||
return "";
|
||
}
|
||
|
||
async function submitWithdraw() {
|
||
if (currentMode !== "withdraw") {
|
||
showToast("exchange_unavailable");
|
||
return;
|
||
}
|
||
|
||
const errorKey = validateWithdraw();
|
||
if (errorKey) {
|
||
showToast(errorKey);
|
||
return;
|
||
}
|
||
|
||
isSubmitting = true;
|
||
updateSubmitState();
|
||
try {
|
||
await requestJSON("/wallet/bank/withdraw", {
|
||
method: "POST",
|
||
body: {
|
||
amount: activeAmount(),
|
||
acceptBankCardId: selectedBankCard.id,
|
||
salaryType: "HOST_SALARY"
|
||
}
|
||
});
|
||
showToast("withdraw_success");
|
||
await fetchBalance();
|
||
} catch (error) {
|
||
console.error("Failed to submit withdrawal:", error);
|
||
showToast(error.message || message("withdraw_failed", "Withdrawal 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 (amountInput) amountInput.value = formatMoney(availableBalance);
|
||
updateSubmitState();
|
||
});
|
||
|
||
amountInput?.addEventListener("input", normalizeAmountInput);
|
||
submitButton?.addEventListener("click", submitWithdraw);
|
||
|
||
syncBackFallback();
|
||
applyLanguage(currentLang);
|
||
setMode("withdraw");
|
||
fetchBalance();
|
||
fetchBankCards();
|
||
})();
|