2026-05-01 00:30:30 +08:00

946 lines
24 KiB
JavaScript

const query = readURLParams();
const storedAppId = window.localStorage.getItem("mifa-pay-application-id") || "";
const API_BASE_URL = "https://jvapi.haiyihy.com";
const LOCAL_API_BASE_URL = "http://127.0.0.1:1100";
const CHANNEL_LOGOS = [
["qris", "./logos/qris.png"],
["dana", "./logos/dana.png"],
["linkaja", "./logos/linkaja.png"],
["ovo", "./logos/ovo.png"],
["shopeepay", "./logos/shopeepay.png"],
["bca", "./logos/bca.png"],
["bni", "./logos/bni.png"],
["permata", "./logos/permata.png"],
["mandiri", "./logos/mandiri.png"],
["bri", "./logos/bri.png"],
["upi", "./logos/upi.png"],
["paytm", "./logos/paytm.png"],
["phonepe", "./logos/phonepe.png"],
["googlepay", "./logos/googlepay.png"],
["bhim", "./logos/bhim.png"],
["mobikwik", "./logos/mobikwik.png"],
["jazzcash", "./logos/jazzcash.png"],
["easypaisa", "./logos/easypaisa.png"],
["vietqr", "./logos/vietqr.png"],
["vnbanktransfer", "./logos/vn-banktransfer.png"],
];
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 getPathApplicationId() {
const segments = window.location.pathname.split("/").filter(Boolean);
const pageIndex = segments.lastIndexOf("mifa-pay");
if (pageIndex === -1) {
return "";
}
const candidate = segments[pageIndex + 1] || "";
return /^\d+$/.test(candidate) ? candidate : "";
}
const defaults = {
applicationId:
getPathApplicationId() ||
query.get("applicationId") ||
query.get("appId") ||
storedAppId ||
"2048200000000000701",
productType: query.get("type") || "GOLD",
};
const state = {
applicationId: defaults.applicationId,
productType: defaults.productType,
application: null,
userProfile: null,
countries: [],
regionId: "",
selectedCountryId: "",
commodityCard: null,
loading: true,
unsupportedRegion: false,
selectedGoodsId: "",
selectedGoods: null,
selectedChannelCode: "",
pendingChannelCode: "",
};
const els = {
applicationId: document.getElementById("applicationId"),
productList: document.getElementById("productList"),
channelList: document.getElementById("channelList"),
unsupportedMessage: document.getElementById("unsupportedMessage"),
goRecharge: document.getElementById("goRecharge"),
pageStatus: document.getElementById("pageStatus"),
};
function setStatus(text = "") {
if (!text) {
els.pageStatus.textContent = "";
els.pageStatus.className = "status";
return;
}
els.pageStatus.textContent = text;
els.pageStatus.className = "status show";
}
function persistSettings() {
window.localStorage.setItem("mifa-pay-application-id", state.applicationId);
}
function readRawParam(name) {
const queries = [window.location.search.replace(/^\?/, "")];
if (window.location.hash.includes("?")) {
queries.push(window.location.hash.split("?")[1]);
}
for (const item of queries) {
for (const part of item.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 apiBaseURL() {
const useProdApi = query.get("prod") === "true";
const isLocalPage =
window.location.protocol === "http:" &&
["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"].includes(
window.location.hostname
);
if (isLocalPage && !useProdApi) {
return LOCAL_API_BASE_URL;
}
if (useProdApi) {
return API_BASE_URL;
}
const override = window.MIFAPAY_API_BASE_URL || query.get("apiBase");
return String(override || API_BASE_URL).replace(/\/+$/, "");
}
function normalizeAuthorization(value) {
const token = String(value || "").trim();
if (!token) {
return "";
}
return /^bearer\s+/i.test(token) ? token : `Bearer ${token}`;
}
function currentToken() {
return (
readRawParam("token") ||
readRawParam("Authorization") ||
readRawParam("authorization") ||
readRawParam("accessToken")
);
}
function currentLanguage() {
return query.get("lang") || document.documentElement.lang || navigator.language || "en";
}
function currentSysOrigin() {
return String(state.application?.appCode || query.get("sysOrigin") || query.get("origin") || "LIKEI")
.trim()
.toUpperCase();
}
function authHeadersFromToken(token) {
const sysOrigin = currentSysOrigin();
return {
Accept: "application/json, text/plain, */*",
Authorization: normalizeAuthorization(token),
"req-lang": currentLanguage(),
"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=${sysOrigin};originChild=${sysOrigin}`,
};
}
function requestHeaders(hasBody = false) {
const token = currentToken();
if (!token) {
throw new Error("Missing token");
}
const headers = authHeadersFromToken(token);
if (hasBody) {
headers["Content-Type"] = "application/json";
}
return headers;
}
function buildRequestURL(path) {
const base = `${apiBaseURL()}/`;
const target = String(path || "").replace(/^\/+/, "");
return new URL(target, base).toString();
}
async function requestJSON(path, options = {}) {
const hasBody = options.body !== undefined;
const request = {
method: options.method || "GET",
cache: "no-store",
headers: requestHeaders(hasBody),
};
if (hasBody) {
request.body = JSON.stringify(options.body);
}
const response = await fetch(buildRequestURL(path), 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;
}
function apiGet(path) {
return requestJSON(path);
}
function apiPost(path, body) {
return requestJSON(path, {
method: "POST",
body,
});
}
function extractErrorMessage(error) {
return (
error?.response?.errorMsg ||
error?.response?.body?.errorMsg ||
error?.errorMsg ||
error?.message ||
"Unknown error"
);
}
function escapeHTML(value) {
return String(value ?? "").replace(/[&<>"']/g, (char) => {
const entities = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;",
};
return entities[char];
});
}
function escapeAttribute(value) {
return escapeHTML(value).replace(/`/g, "&#96;");
}
function readStoredUserProfile() {
const raw = window.localStorage.getItem("userProfile");
if (!raw) {
return null;
}
try {
const parsed = JSON.parse(raw);
return parsed && typeof parsed === "object" ? parsed : null;
} catch (error) {
return null;
}
}
function resolveUserProfile(response = null) {
if (response?.body && typeof response.body === "object") {
return response.body;
}
return readStoredUserProfile();
}
function currentUserId() {
return String(state.userProfile?.id || "").trim();
}
function currentCountry() {
return (
state.countries.find((item) => String(item.id) === String(state.selectedCountryId)) ||
null
);
}
function currentPayCountryId() {
const selected = currentCountry();
return String(selected?.payCountryId || selected?.id || state.selectedCountryId || "");
}
function currentRegionId() {
return state.regionId;
}
function currentUserAccount() {
return String(
state.userProfile?.account ||
state.userProfile?.actualAccount ||
state.userProfile?.ownSpecialId?.account ||
state.userProfile?.specialId?.account ||
""
).trim();
}
function getCountryLabel(item) {
return (
item?.label ||
item?.countryName ||
item?.country?.aliasName ||
item?.country?.countryName ||
String(item?.id || "")
);
}
function getGoodsList() {
return state.commodityCard?.commodity || [];
}
function getAllChannelList() {
return state.commodityCard?.channels || [];
}
function toNumber(value, fallback = 0) {
const number = Number(value);
return Number.isFinite(number) ? number : fallback;
}
function channelSupportsGoods(channel, goods) {
if (!goods) {
return true;
}
const details = channel?.details || {};
const amountUsd = toNumber(goods?.amountUsd || goods?.amount);
const min = toNumber(details.factoryMinLimit, 0);
const max = toNumber(details.factoryMaxLimit, Number.MAX_SAFE_INTEGER);
const daily = toNumber(details.factoryDailyLimit, Number.MAX_SAFE_INTEGER);
return amountUsd >= min && amountUsd <= max && amountUsd <= daily;
}
function getChannelList(goods = state.selectedGoods) {
return getAllChannelList().filter((item) => channelSupportsGoods(item, goods));
}
function goodsHasChannel(goods) {
return getChannelList(goods).length > 0;
}
function hasAvailablePayment() {
return getGoodsList().some((goods) => goodsHasChannel(goods));
}
function setUnsupportedRegion(value) {
state.unsupportedRegion = Boolean(value);
renderUnsupportedState();
}
function renderUnsupportedState() {
const show = !state.loading && state.unsupportedRegion;
els.unsupportedMessage.hidden = !show;
els.unsupportedMessage.className = show
? "unsupported-state show"
: "unsupported-state";
els.productList.hidden = show;
els.channelList.hidden = show;
if (show) {
els.goRecharge.hidden = true;
els.productList.innerHTML = "";
els.channelList.innerHTML = "";
}
}
function getChannelCode(item) {
return item?.details?.channelCode || item?.channel?.channelCode || "";
}
function getChannelName(item) {
return String(
item?.channel?.channelName ||
item?.details?.factoryChannel ||
item?.details?.channelCode ||
item?.channel?.channelCode ||
"Pay"
).trim();
}
function normalizeLogoKey(value) {
return String(value || "")
.trim()
.toLowerCase()
.replace(/[^a-z0-9]/g, "");
}
function getChannelLogoCandidates(item) {
const values = [
item?.channel?.channelCode,
item?.details?.channelCode,
item?.details?.factoryChannel,
item?.channel?.channelName,
getChannelName(item),
];
const keys = new Set();
values.filter(Boolean).forEach((value) => {
const text = String(value);
const normalized = normalizeLogoKey(text);
if (normalized) {
keys.add(normalized);
}
text
.split(/[_\s/-]+/)
.map(normalizeLogoKey)
.filter(Boolean)
.forEach((key) => keys.add(key));
});
return Array.from(keys);
}
function getLocalChannelIconURL(item) {
const candidates = getChannelLogoCandidates(item);
const exact = CHANNEL_LOGOS.find(([key]) => candidates.includes(key));
if (exact) {
return exact[1];
}
const combined = candidates.join(" ");
const partial = CHANNEL_LOGOS.find(([key]) => combined.includes(key));
return partial?.[1] || "";
}
function getChannelIconURL(item) {
return String(
item?.channel?.channelIcon || item?.details?.channelIcon || getLocalChannelIconURL(item)
).trim();
}
function getChannelAbbr(item) {
const candidates = [getChannelCode(item), getChannelName(item)];
for (const value of candidates) {
const segments = String(value || "")
.split(/[_\s/-]+/)
.map((segment) => segment.replace(/[^A-Za-z0-9]/g, ""))
.filter(Boolean);
if (segments.length) {
return segments[segments.length - 1].slice(0, 5).toUpperCase();
}
}
return "PAY";
}
function channelIconHTML(item) {
const name = getChannelName(item);
const icon = getChannelIconURL(item);
if (icon) {
return `<span class="channel-icon"><img src="${escapeAttribute(icon)}" alt="${escapeAttribute(
name
)}" /></span>`;
}
return `<span class="channel-icon channel-fallback">${escapeHTML(getChannelAbbr(item))}</span>`;
}
function getAmountText(item) {
const content = item?.content || item?.obtainGoldsQuantity || "-";
return `${content} ${item?.type || ""}`.trim();
}
function currentCurrency(item = null) {
return String(
item?.currency || state.commodityCard?.payCountry?.currency || currentCountry()?.currency || "USD"
)
.trim()
.toUpperCase();
}
function formatMoney(value, currency = "") {
if (value === null || value === undefined || value === "") {
return "-";
}
const number = Number(value);
if (!Number.isFinite(number)) {
return String(value);
}
const zeroDecimalCurrencies = new Set(["IDR", "JPY", "KRW", "VND"]);
const maximumFractionDigits = zeroDecimalCurrencies.has(currency) ? 0 : 2;
return number.toLocaleString("en-US", {
minimumFractionDigits: 0,
maximumFractionDigits,
});
}
function getPriceText(item) {
const currency = currentCurrency(item);
return `${currency} ${formatMoney(item?.amount ?? item?.amountUsd, currency)}`;
}
function getUsdPriceText(item) {
return `USD ${formatMoney(item?.amountUsd ?? item?.amount ?? "-", "USD")}`;
}
function getCountryCodes(item) {
const country = item?.country || {};
return [
item?.countryCode,
country?.alphaTwo,
country?.alphaThree,
...(Array.isArray(country?.countryCodeAliases) ? country.countryCodeAliases : []),
]
.filter(Boolean)
.map((value) => String(value).trim().toUpperCase())
.filter(Boolean);
}
function getCountryId(item) {
return String(item?.countryId || item?.country?.id || "").trim();
}
function mergeCompatibleCountries(regionCountries, openCountries) {
const compatibleCountryMap = new Map();
openCountries.forEach((item) => {
const countryId = getCountryId(item);
if (countryId && !compatibleCountryMap.has(countryId)) {
compatibleCountryMap.set(countryId, item);
}
});
return regionCountries.map((item) => {
if (item?.payCountryId) {
return item;
}
const compatibleCountry = compatibleCountryMap.get(getCountryId(item));
if (!compatibleCountry) {
return item;
}
return {
...item,
payCountryId: compatibleCountry.payCountryId || compatibleCountry.id,
currency: item.currency || compatibleCountry.currency,
};
});
}
function ensureSelectedGoods() {
const goods = getGoodsList();
if (!goods.length) {
state.selectedGoodsId = "";
state.selectedGoods = null;
return;
}
const matched =
goods.find((item) => String(item.id) === state.selectedGoodsId) ||
goods.find((item) => item.type === state.productType) ||
goods[0];
const payableGoods = goodsHasChannel(matched)
? matched
: goods.find((item) => goodsHasChannel(item)) || matched;
state.selectedGoods = payableGoods;
state.selectedGoodsId = String(payableGoods.id);
}
function renderProductLoading() {
els.productList.innerHTML = `
<div class="amount-item skeleton" aria-hidden="true">
<span class="amount-copy">
<span class="skeleton-line strong"></span>
<span class="skeleton-line short"></span>
</span>
<span class="price-copy">
<span class="skeleton-line price"></span>
<span class="skeleton-line short"></span>
</span>
</div>
`;
}
function renderChannelLoading() {
els.goRecharge.hidden = true;
els.channelList.innerHTML = Array.from({ length: 8 })
.map(
() => `
<div class="channel-button skeleton" aria-hidden="true">
<span class="skeleton-icon"></span>
<span class="skeleton-line short"></span>
</div>
`
)
.join("");
}
function renderProducts() {
if (state.loading) {
renderProductLoading();
return;
}
if (state.unsupportedRegion) {
els.productList.innerHTML = "";
return;
}
const goods = getGoodsList();
ensureSelectedGoods();
if (!goods.length) {
els.productList.innerHTML = '<div class="empty">No amount available.</div>';
return;
}
els.productList.innerHTML = goods
.map((item) => {
const payable = goodsHasChannel(item);
const selected = payable && String(item.id) === state.selectedGoodsId;
const amount = item?.content || item?.obtainGoldsQuantity || "-";
const type = item?.type || "Recharge";
return `
<button
type="button"
class="amount-item${selected ? " selected" : ""}"
data-goods-id="${escapeAttribute(item.id)}"
${payable ? "" : "disabled"}
>
<span class="check"></span>
<span class="amount-copy">
<span class="amount-value">
<span class="coin-icon" aria-hidden="true"></span>
${escapeHTML(amount)}
</span>
<span class="amount-meta">${escapeHTML(type)}</span>
</span>
<span class="price-copy">
<strong>${escapeHTML(getPriceText(item))}</strong>
<span>${escapeHTML(getUsdPriceText(item))}</span>
</span>
</button>
`;
})
.join("");
els.productList.querySelectorAll("[data-goods-id]").forEach((button) => {
button.addEventListener("click", () => {
const goodsId = button.getAttribute("data-goods-id");
state.selectedGoodsId = goodsId;
state.selectedGoods =
getGoodsList().find((item) => String(item.id) === goodsId) || null;
renderProducts();
renderChannels();
});
});
}
function ensureSelectedChannel(channels) {
if (!channels.length) {
state.selectedChannelCode = "";
return;
}
const selectedExists = channels.some(
(item) => String(getChannelCode(item)) === String(state.selectedChannelCode)
);
if (!selectedExists) {
state.selectedChannelCode = getChannelCode(channels[0]);
}
}
function renderGoRecharge(channels = getChannelList()) {
const hasSelection = channels.some(
(item) => String(getChannelCode(item)) === String(state.selectedChannelCode)
);
els.goRecharge.hidden = !channels.length;
els.goRecharge.disabled = !hasSelection || Boolean(state.pendingChannelCode);
els.goRecharge.textContent = state.pendingChannelCode ? "Processing..." : "Go Recharge";
}
function renderChannels() {
if (state.loading) {
renderChannelLoading();
return;
}
if (state.unsupportedRegion) {
els.channelList.innerHTML = "";
els.goRecharge.hidden = true;
return;
}
const channels = getChannelList();
ensureSelectedChannel(channels);
renderGoRecharge(channels);
if (!channels.length) {
els.channelList.innerHTML = '<div class="empty">No payment method available.</div>';
return;
}
els.channelList.innerHTML = channels
.map((item) => {
const code = getChannelCode(item);
const selected = String(code) === String(state.selectedChannelCode);
const name = getChannelName(item);
return `
<button
type="button"
class="channel-button${selected ? " selected" : ""}"
data-channel-code="${escapeAttribute(code)}"
aria-pressed="${selected ? "true" : "false"}"
${state.pendingChannelCode ? "disabled" : ""}
>
${channelIconHTML(item)}
<span class="channel-name">${escapeHTML(name)}</span>
</button>
`;
})
.join("");
els.channelList.querySelectorAll("[data-channel-code]").forEach((button) => {
button.addEventListener("click", () => {
const channelCode = button.getAttribute("data-channel-code");
state.selectedChannelCode = channelCode;
setStatus("");
renderChannels();
});
});
}
function selectCountryByProfile() {
const userCountryCode = String(state.userProfile?.countryCode || "").trim().toUpperCase();
const userCountryId = String(state.userProfile?.countryId || "").trim();
if (!userCountryCode && !userCountryId) {
return false;
}
const matched = state.countries.find((item) => {
const itemCountryId = getCountryId(item);
if (userCountryId && itemCountryId === userCountryId) {
return true;
}
return userCountryCode && getCountryCodes(item).includes(userCountryCode);
});
if (!matched) {
return false;
}
state.selectedCountryId = String(matched.id);
return true;
}
async function connectSession() {
const response = await apiGet("/team/member/profile");
const userProfile = resolveUserProfile(response);
if (!userProfile?.id) {
throw new Error("Account not available");
}
state.userProfile = userProfile;
}
async function refreshMeta() {
if (!state.applicationId) {
throw new Error("Missing applicationId");
}
persistSettings();
els.applicationId.value = state.applicationId;
const applicationResp = await apiGet(
`/order/web/pay/application/${encodeURIComponent(state.applicationId)}`
);
state.application = applicationResp?.body || {};
const profileQuery = new URLSearchParams({
sysOrigin: state.application?.appCode || state.userProfile?.originSys || "",
type: state.productType,
});
const account = currentUserAccount();
if (account) {
profileQuery.set("account", account);
}
if (currentUserId()) {
profileQuery.set("userId", currentUserId());
}
const profileResp = await apiGet(
`/order/web/pay/user-profile?${profileQuery.toString()}`
);
const payProfile = profileResp?.body || {};
if (payProfile.userProfile) {
state.userProfile = {
...state.userProfile,
...payProfile.userProfile,
};
}
state.regionId = payProfile.regionId || "";
state.countries = Array.isArray(payProfile.countryList) ? payProfile.countryList : [];
if (!state.countries.length) {
state.selectedCountryId = "";
state.commodityCard = null;
state.loading = false;
setUnsupportedRegion(true);
renderProducts();
renderChannels();
setStatus("");
return;
}
if (state.countries.some((item) => !item?.payCountryId)) {
const countryResp = await apiGet("/order/web/pay/country");
const openCountries = Array.isArray(countryResp?.body) ? countryResp.body : [];
state.countries = mergeCompatibleCountries(state.countries, openCountries);
}
if (
!state.selectedCountryId ||
!state.countries.some((item) => String(item.id) === String(state.selectedCountryId))
) {
if (!selectCountryByProfile()) {
state.selectedCountryId = String(state.countries[0].id);
}
}
const commodityResp = await apiGet(
`/order/web/pay/commodity?applicationId=${encodeURIComponent(
state.applicationId
)}&payCountryId=${encodeURIComponent(currentPayCountryId())}&type=${encodeURIComponent(
state.productType
)}&regionId=${encodeURIComponent(currentRegionId())}`
);
state.commodityCard = commodityResp?.body || {};
state.loading = false;
setUnsupportedRegion(!hasAvailablePayment());
renderProducts();
renderChannels();
setStatus("");
}
function safeDecodeUrl(url) {
if (!url) {
return "";
}
try {
return decodeURIComponent(url);
} catch (error) {
return url;
}
}
async function createOrder(channelCode) {
if (!state.selectedGoods) {
throw new Error("Select an amount first");
}
if (!channelCode) {
throw new Error("Select a payment method first");
}
if (!currentUserId()) {
throw new Error("Account not available");
}
state.selectedChannelCode = channelCode;
state.pendingChannelCode = channelCode;
setStatus("");
renderChannels();
const payload = {
applicationId: state.applicationId,
goodsId: state.selectedGoodsId,
payCountryId: currentPayCountryId(),
userId: currentUserId(),
channelCode,
newVersion: true,
};
const response = await apiPost("/order/web/pay/recharge", payload);
const requestUrl = safeDecodeUrl(response?.body?.requestUrl || "");
if (!requestUrl) {
throw new Error("Missing payment url");
}
window.location.href = requestUrl;
}
async function reloadPageData() {
await connectSession();
await refreshMeta();
}
async function init() {
state.loading = true;
setUnsupportedRegion(false);
renderProducts();
renderChannels();
els.goRecharge.addEventListener("click", async () => {
try {
await createOrder(state.selectedChannelCode);
} catch (error) {
state.pendingChannelCode = "";
renderChannels();
setStatus(extractErrorMessage(error));
}
});
try {
await reloadPageData();
} catch (error) {
state.loading = false;
setUnsupportedRegion(false);
renderProducts();
renderChannels();
setStatus(extractErrorMessage(error));
}
}
init();