946 lines
24 KiB
JavaScript
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 = {
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
'"': """,
|
|
"'": "'",
|
|
};
|
|
return entities[char];
|
|
});
|
|
}
|
|
|
|
function escapeAttribute(value) {
|
|
return escapeHTML(value).replace(/`/g, "`");
|
|
}
|
|
|
|
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
|
|
)}®ionId=${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();
|