充值接入
@ -2,7 +2,29 @@ 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 LOGO_SRC = "./mifapay-logo.png";
|
||||
|
||||
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);
|
||||
@ -46,8 +68,11 @@ const state = {
|
||||
regionId: "",
|
||||
selectedCountryId: "",
|
||||
commodityCard: null,
|
||||
loading: true,
|
||||
unsupportedRegion: false,
|
||||
selectedGoodsId: "",
|
||||
selectedGoods: null,
|
||||
selectedChannelCode: "",
|
||||
pendingChannelCode: "",
|
||||
};
|
||||
|
||||
@ -55,6 +80,8 @@ 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"),
|
||||
};
|
||||
|
||||
@ -146,7 +173,14 @@ 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),
|
||||
@ -155,7 +189,7 @@ function authHeadersFromToken(token) {
|
||||
"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",
|
||||
"req-sys-origin": `origin=${sysOrigin};originChild=${sysOrigin}`,
|
||||
};
|
||||
}
|
||||
|
||||
@ -224,6 +258,23 @@ function extractErrorMessage(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) {
|
||||
@ -250,8 +301,16 @@ function currentUserId() {
|
||||
return String(state.userProfile?.id || "").trim();
|
||||
}
|
||||
|
||||
function currentCountryId() {
|
||||
return state.selectedCountryId;
|
||||
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() {
|
||||
@ -282,22 +341,230 @@ function getGoodsList() {
|
||||
return state.commodityCard?.commodity || [];
|
||||
}
|
||||
|
||||
function getChannelList() {
|
||||
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 = item?.currency || state.commodityCard?.payCountry?.currency || "USD";
|
||||
return `${item?.amount || item?.amountUsd || "-"} ${currency}`;
|
||||
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() {
|
||||
@ -312,11 +579,53 @@ function ensureSelectedGoods() {
|
||||
goods.find((item) => String(item.id) === state.selectedGoodsId) ||
|
||||
goods.find((item) => item.type === state.productType) ||
|
||||
goods[0];
|
||||
state.selectedGoods = matched;
|
||||
state.selectedGoodsId = String(matched.id);
|
||||
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();
|
||||
|
||||
@ -327,21 +636,28 @@ function renderProducts() {
|
||||
|
||||
els.productList.innerHTML = goods
|
||||
.map((item) => {
|
||||
const selected = String(item.id) === state.selectedGoodsId;
|
||||
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="${item.id}"
|
||||
data-goods-id="${escapeAttribute(item.id)}"
|
||||
${payable ? "" : "disabled"}
|
||||
>
|
||||
<span class="check"></span>
|
||||
<span class="amount-copy">
|
||||
<span class="amount-value">${item.content || item.obtainGoldsQuantity || "-"}</span>
|
||||
<span class="amount-meta">${item.type || "Recharge"}</span>
|
||||
<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>${item.amount || item.amountUsd || "-"}</strong>
|
||||
<span>USD ${item.amountUsd || "-"}</span>
|
||||
<strong>${escapeHTML(getPriceText(item))}</strong>
|
||||
<span>${escapeHTML(getUsdPriceText(item))}</span>
|
||||
</span>
|
||||
</button>
|
||||
`;
|
||||
@ -355,12 +671,50 @@ function renderProducts() {
|
||||
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;
|
||||
@ -369,46 +723,47 @@ function renderChannels() {
|
||||
els.channelList.innerHTML = channels
|
||||
.map((item) => {
|
||||
const code = getChannelCode(item);
|
||||
const pending = state.pendingChannelCode === code;
|
||||
const selected = String(code) === String(state.selectedChannelCode);
|
||||
const name = getChannelName(item);
|
||||
return `
|
||||
<button
|
||||
type="button"
|
||||
class="channel-button"
|
||||
data-channel-code="${code}"
|
||||
${pending ? "disabled" : ""}
|
||||
class="channel-button${selected ? " selected" : ""}"
|
||||
data-channel-code="${escapeAttribute(code)}"
|
||||
aria-pressed="${selected ? "true" : "false"}"
|
||||
${state.pendingChannelCode ? "disabled" : ""}
|
||||
>
|
||||
<span class="channel-logo">
|
||||
<img src="${LOGO_SRC}" alt="MiFaPay" />
|
||||
</span>
|
||||
<span class="channel-name">${pending ? "Processing..." : "Pay with MiFaPay"}</span>
|
||||
${channelIconHTML(item)}
|
||||
<span class="channel-name">${escapeHTML(name)}</span>
|
||||
</button>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
els.channelList.querySelectorAll("[data-channel-code]").forEach((button) => {
|
||||
button.addEventListener("click", async () => {
|
||||
button.addEventListener("click", () => {
|
||||
const channelCode = button.getAttribute("data-channel-code");
|
||||
try {
|
||||
await createOrder(channelCode);
|
||||
} catch (error) {
|
||||
state.pendingChannelCode = "";
|
||||
renderChannels();
|
||||
setStatus(extractErrorMessage(error));
|
||||
}
|
||||
state.selectedChannelCode = channelCode;
|
||||
setStatus("");
|
||||
renderChannels();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function selectCountryByProfile() {
|
||||
const userCountryCode = state.userProfile?.countryCode;
|
||||
if (!userCountryCode) {
|
||||
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) =>
|
||||
getCountryLabel(item).toUpperCase().includes(userCountryCode.toUpperCase())
|
||||
);
|
||||
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;
|
||||
@ -469,9 +824,18 @@ async function refreshMeta() {
|
||||
if (!state.countries.length) {
|
||||
state.selectedCountryId = "";
|
||||
state.commodityCard = null;
|
||||
state.loading = false;
|
||||
setUnsupportedRegion(true);
|
||||
renderProducts();
|
||||
renderChannels();
|
||||
throw new Error("No supported country");
|
||||
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 (
|
||||
@ -486,12 +850,14 @@ async function refreshMeta() {
|
||||
const commodityResp = await apiGet(
|
||||
`/order/web/pay/commodity?applicationId=${encodeURIComponent(
|
||||
state.applicationId
|
||||
)}&payCountryId=${encodeURIComponent(currentCountryId())}&type=${encodeURIComponent(
|
||||
)}&payCountryId=${encodeURIComponent(currentPayCountryId())}&type=${encodeURIComponent(
|
||||
state.productType
|
||||
)}®ionId=${encodeURIComponent(currentRegionId())}`
|
||||
);
|
||||
|
||||
state.commodityCard = commodityResp?.body || {};
|
||||
state.loading = false;
|
||||
setUnsupportedRegion(!hasAvailablePayment());
|
||||
renderProducts();
|
||||
renderChannels();
|
||||
setStatus("");
|
||||
@ -514,10 +880,15 @@ async function createOrder(channelCode) {
|
||||
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();
|
||||
@ -525,7 +896,7 @@ async function createOrder(channelCode) {
|
||||
const payload = {
|
||||
applicationId: state.applicationId,
|
||||
goodsId: state.selectedGoodsId,
|
||||
payCountryId: currentCountryId(),
|
||||
payCountryId: currentPayCountryId(),
|
||||
userId: currentUserId(),
|
||||
channelCode,
|
||||
newVersion: true,
|
||||
@ -546,12 +917,27 @@ async function reloadPageData() {
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
BIN
h5/mifa-pay/bg.png
Normal file
|
After Width: | Height: | Size: 533 KiB |
@ -12,10 +12,13 @@
|
||||
--bg: #08131f;
|
||||
--surface: rgba(15, 28, 42, 0.92);
|
||||
--surface-strong: #102030;
|
||||
--line: rgba(214, 170, 82, 0.18);
|
||||
--line-strong: rgba(214, 170, 82, 0.42);
|
||||
--line: rgba(72, 224, 208, 0.2);
|
||||
--line-strong: rgba(72, 224, 208, 0.78);
|
||||
--text: #f6f1e6;
|
||||
--muted: #bcae8d;
|
||||
--muted: #bed4d1;
|
||||
--accent: #48e0d0;
|
||||
--accent-strong: #1baaa3;
|
||||
--accent-soft: rgba(72, 224, 208, 0.16);
|
||||
--gold: #d6aa52;
|
||||
--gold-strong: #c49335;
|
||||
--danger: #ff9f9f;
|
||||
@ -38,8 +41,9 @@
|
||||
sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(214, 170, 82, 0.1), transparent 24%),
|
||||
linear-gradient(180deg, #091321 0%, var(--bg) 100%);
|
||||
linear-gradient(180deg, rgba(8, 19, 31, 0.44), rgba(8, 19, 31, 0.72)),
|
||||
url("./bg.png") center top / cover no-repeat,
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
button {
|
||||
@ -59,9 +63,12 @@
|
||||
font-size: 30px;
|
||||
line-height: 1;
|
||||
font-weight: 800;
|
||||
color: var(--gold);
|
||||
color: #f8fbff;
|
||||
text-align: center;
|
||||
letter-spacing: 0.03em;
|
||||
text-shadow:
|
||||
0 2px 10px rgba(8, 19, 31, 0.88),
|
||||
0 0 18px rgba(72, 224, 208, 0.3);
|
||||
}
|
||||
|
||||
.list {
|
||||
@ -69,39 +76,48 @@
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.amount-item,
|
||||
.channel-button {
|
||||
.amount-item {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 16px 18px;
|
||||
padding: 16px 54px 16px 18px;
|
||||
border-radius: 22px;
|
||||
border: 1px solid var(--line);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(214, 170, 82, 0.04), transparent 60%),
|
||||
linear-gradient(180deg, rgba(72, 224, 208, 0.07), transparent 62%),
|
||||
var(--surface);
|
||||
box-shadow: var(--shadow);
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
transition:
|
||||
transform 0.18s ease,
|
||||
background 0.18s ease,
|
||||
border-color 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.amount-item:active,
|
||||
.channel-button:active {
|
||||
.amount-item:active {
|
||||
transform: scale(0.988);
|
||||
}
|
||||
|
||||
.amount-item.selected {
|
||||
border-color: var(--line-strong);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(72, 224, 208, 0.22), rgba(13, 30, 44, 0.96)),
|
||||
var(--surface-strong);
|
||||
transform: scale(1.012);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(214, 170, 82, 0.12),
|
||||
0 16px 36px rgba(0, 0, 0, 0.24);
|
||||
0 0 0 1px rgba(72, 224, 208, 0.22),
|
||||
0 0 22px rgba(72, 224, 208, 0.28),
|
||||
0 16px 36px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
.amount-item:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.amount-copy {
|
||||
@ -111,12 +127,37 @@
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.coin-icon {
|
||||
position: relative;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 50%;
|
||||
background:
|
||||
radial-gradient(circle at 35% 28%, #fff5b8 0 18%, transparent 19%),
|
||||
linear-gradient(135deg, #ffd96a, #d79a26);
|
||||
box-shadow:
|
||||
inset 0 0 0 2px rgba(142, 91, 13, 0.46),
|
||||
0 0 12px rgba(255, 217, 106, 0.36);
|
||||
}
|
||||
|
||||
.coin-icon::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 6px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(122, 78, 10, 0.48);
|
||||
}
|
||||
|
||||
.amount-meta {
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
@ -130,12 +171,14 @@
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
max-width: 46%;
|
||||
}
|
||||
|
||||
.price-copy strong {
|
||||
font-size: 22px;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
color: var(--gold);
|
||||
color: var(--accent);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.price-copy span {
|
||||
@ -145,17 +188,20 @@
|
||||
|
||||
.check {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 14px;
|
||||
top: 50%;
|
||||
right: 18px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(214, 170, 82, 0.3);
|
||||
border: 1px solid rgba(72, 224, 208, 0.36);
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.amount-item.selected .check {
|
||||
border-color: transparent;
|
||||
background: linear-gradient(135deg, #efcf82, var(--gold-strong));
|
||||
background: linear-gradient(135deg, #8cf5ec, var(--accent-strong));
|
||||
box-shadow: 0 0 14px rgba(72, 224, 208, 0.45);
|
||||
}
|
||||
|
||||
.amount-item.selected .check::after {
|
||||
@ -174,32 +220,107 @@
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.channel-button {
|
||||
justify-content: flex-start;
|
||||
padding-right: 18px;
|
||||
.channel-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.channel-logo {
|
||||
width: 78px;
|
||||
height: 44px;
|
||||
.channel-button {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 84px;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 6px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--line);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(72, 224, 208, 0.06), transparent 62%),
|
||||
var(--surface);
|
||||
box-shadow: var(--shadow);
|
||||
color: var(--text);
|
||||
text-align: center;
|
||||
transition:
|
||||
transform 0.18s ease,
|
||||
background 0.18s ease,
|
||||
border-color 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.channel-button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.channel-button.selected {
|
||||
border-color: var(--line-strong);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(72, 224, 208, 0.28), rgba(13, 30, 44, 0.97)),
|
||||
var(--surface-strong);
|
||||
transform: translateY(-2px) scale(1.045);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(72, 224, 208, 0.26),
|
||||
0 0 22px rgba(72, 224, 208, 0.34),
|
||||
0 18px 40px rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
|
||||
.channel-button.selected::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 12px rgba(72, 224, 208, 0.68);
|
||||
}
|
||||
|
||||
.channel-icon {
|
||||
width: 54px;
|
||||
height: 38px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: 14px;
|
||||
padding: 4px;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.channel-logo img {
|
||||
.channel-icon img {
|
||||
display: block;
|
||||
width: 68px;
|
||||
height: auto;
|
||||
max-width: 48px;
|
||||
max-height: 30px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.channel-fallback {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
padding: 0;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #8cf5ec, var(--accent-strong));
|
||||
color: #0c1520;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.channel-name {
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
min-height: 24px;
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.channel-button:disabled {
|
||||
@ -207,6 +328,32 @@
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.recharge-button {
|
||||
width: 100%;
|
||||
margin-top: 14px;
|
||||
padding: 14px 18px;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(135deg, #8cf5ec, var(--accent-strong));
|
||||
color: #0c1520;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
box-shadow:
|
||||
0 0 18px rgba(72, 224, 208, 0.28),
|
||||
0 16px 32px rgba(0, 0, 0, 0.28);
|
||||
transition:
|
||||
transform 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.recharge-button:active {
|
||||
transform: scale(0.985);
|
||||
}
|
||||
|
||||
.recharge-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: none;
|
||||
margin-top: 14px;
|
||||
@ -225,6 +372,7 @@
|
||||
}
|
||||
|
||||
.empty {
|
||||
grid-column: 1 / -1;
|
||||
padding: 16px;
|
||||
border-radius: 20px;
|
||||
border: 1px dashed var(--line);
|
||||
@ -232,18 +380,136 @@
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.unsupported-state {
|
||||
display: none;
|
||||
min-height: calc(100vh - 130px);
|
||||
place-items: center;
|
||||
padding: 36px 8px;
|
||||
}
|
||||
|
||||
.unsupported-state.show {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.unsupported-panel {
|
||||
width: 100%;
|
||||
max-width: 360px;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 14px;
|
||||
padding: 28px 22px;
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(72, 224, 208, 0.46);
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(72, 224, 208, 0.18), transparent 58%),
|
||||
rgba(8, 19, 31, 0.82);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(248, 251, 255, 0.06),
|
||||
0 0 28px rgba(72, 224, 208, 0.24),
|
||||
0 18px 44px rgba(0, 0, 0, 0.34);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.unsupported-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #8cf5ec, var(--accent-strong));
|
||||
color: #0c1520;
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
box-shadow: 0 0 18px rgba(72, 224, 208, 0.42);
|
||||
}
|
||||
|
||||
.unsupported-title {
|
||||
margin: 0;
|
||||
max-width: 290px;
|
||||
color: #f8fbff;
|
||||
font-size: 20px;
|
||||
line-height: 1.35;
|
||||
font-weight: 800;
|
||||
text-shadow: 0 2px 10px rgba(8, 19, 31, 0.72);
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.skeleton::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
transform: translateX(-100%);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(248, 251, 255, 0.12),
|
||||
transparent
|
||||
);
|
||||
animation: shimmer 1.25s infinite;
|
||||
}
|
||||
|
||||
.skeleton-line,
|
||||
.skeleton-icon {
|
||||
display: block;
|
||||
border-radius: 999px;
|
||||
background: rgba(248, 251, 255, 0.12);
|
||||
}
|
||||
|
||||
.skeleton-line.strong {
|
||||
width: 92px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.skeleton-line.short {
|
||||
width: 64px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.skeleton-line.price {
|
||||
width: 104px;
|
||||
height: 18px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.skeleton-icon {
|
||||
width: 54px;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<h1>Recharge</h1>
|
||||
<div id="productList" class="list"></div>
|
||||
<div id="channelList" class="list channel-block"></div>
|
||||
<div id="channelList" class="channel-grid channel-block"></div>
|
||||
<div id="unsupportedMessage" class="unsupported-state" hidden>
|
||||
<div class="unsupported-panel">
|
||||
<div class="unsupported-icon" aria-hidden="true">!</div>
|
||||
<p class="unsupported-title">
|
||||
Sorry, this region is currently not supported.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button id="goRecharge" class="recharge-button" type="button" disabled hidden>
|
||||
Go Recharge
|
||||
</button>
|
||||
<div id="pageStatus" class="status" aria-live="polite"></div>
|
||||
</div>
|
||||
|
||||
<input id="applicationId" type="hidden" />
|
||||
|
||||
<script type="module" src="./app.js"></script>
|
||||
<script type="module" src="./app.js?v=20260501-6"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
BIN
h5/mifa-pay/logos/bca.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
h5/mifa-pay/logos/bhim.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
h5/mifa-pay/logos/bni.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
h5/mifa-pay/logos/bri.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
h5/mifa-pay/logos/dana.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
h5/mifa-pay/logos/easypaisa.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
h5/mifa-pay/logos/googlepay.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
h5/mifa-pay/logos/jazzcash.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
h5/mifa-pay/logos/linkaja.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
h5/mifa-pay/logos/mandiri.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
h5/mifa-pay/logos/mobikwik.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
h5/mifa-pay/logos/ovo.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
h5/mifa-pay/logos/paytm.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
h5/mifa-pay/logos/permata.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
h5/mifa-pay/logos/phonepe.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
h5/mifa-pay/logos/qris.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
h5/mifa-pay/logos/shopeepay.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
h5/mifa-pay/logos/upi.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
h5/mifa-pay/logos/vietqr.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
h5/mifa-pay/logos/vn-banktransfer.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |