充值接入

This commit is contained in:
170-carry 2026-05-01 00:30:30 +08:00
parent 6930596c60
commit 56a36f5fc2
23 changed files with 724 additions and 72 deletions

View File

@ -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 = {
"&": "&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) {
@ -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
)}&regionId=${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

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 KiB

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
h5/mifa-pay/logos/bhim.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
h5/mifa-pay/logos/bni.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
h5/mifa-pay/logos/bri.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
h5/mifa-pay/logos/dana.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
h5/mifa-pay/logos/ovo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
h5/mifa-pay/logos/paytm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
h5/mifa-pay/logos/qris.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
h5/mifa-pay/logos/upi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB