545 lines
14 KiB
JavaScript
545 lines
14 KiB
JavaScript
const query = readURLParams();
|
|
const storedAppId = window.localStorage.getItem("mifa-pay-application-id") || "";
|
|
const API_BASE_URL = "https://jvapi.haiyihy.com";
|
|
const LOGO_SRC = "./mifapay-logo.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,
|
|
selectedGoodsId: "",
|
|
selectedGoods: null,
|
|
pendingChannelCode: "",
|
|
};
|
|
|
|
const els = {
|
|
applicationId: document.getElementById("applicationId"),
|
|
productList: document.getElementById("productList"),
|
|
channelList: document.getElementById("channelList"),
|
|
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 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 authHeadersFromToken(token) {
|
|
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=ATYOU;originChild=ATYOU",
|
|
};
|
|
}
|
|
|
|
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 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 currentCountryId() {
|
|
return 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 getChannelList() {
|
|
return state.commodityCard?.channels || [];
|
|
}
|
|
|
|
function getChannelCode(item) {
|
|
return item?.details?.channelCode || item?.channel?.channelCode || "";
|
|
}
|
|
|
|
function getAmountText(item) {
|
|
const content = item?.content || item?.obtainGoldsQuantity || "-";
|
|
return `${content} ${item?.type || ""}`.trim();
|
|
}
|
|
|
|
function getPriceText(item) {
|
|
const currency = item?.currency || state.commodityCard?.payCountry?.currency || "USD";
|
|
return `${item?.amount || item?.amountUsd || "-"} ${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];
|
|
state.selectedGoods = matched;
|
|
state.selectedGoodsId = String(matched.id);
|
|
}
|
|
|
|
function renderProducts() {
|
|
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 selected = String(item.id) === state.selectedGoodsId;
|
|
return `
|
|
<button
|
|
type="button"
|
|
class="amount-item${selected ? " selected" : ""}"
|
|
data-goods-id="${item.id}"
|
|
>
|
|
<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>
|
|
<span class="price-copy">
|
|
<strong>${item.amount || item.amountUsd || "-"}</strong>
|
|
<span>USD ${item.amountUsd || "-"}</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();
|
|
});
|
|
});
|
|
}
|
|
|
|
function renderChannels() {
|
|
const channels = getChannelList();
|
|
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 pending = state.pendingChannelCode === code;
|
|
return `
|
|
<button
|
|
type="button"
|
|
class="channel-button"
|
|
data-channel-code="${code}"
|
|
${pending ? "disabled" : ""}
|
|
>
|
|
<span class="channel-logo">
|
|
<img src="${LOGO_SRC}" alt="MiFaPay" />
|
|
</span>
|
|
<span class="channel-name">${pending ? "Processing..." : "Pay with MiFaPay"}</span>
|
|
</button>
|
|
`;
|
|
})
|
|
.join("");
|
|
|
|
els.channelList.querySelectorAll("[data-channel-code]").forEach((button) => {
|
|
button.addEventListener("click", async () => {
|
|
const channelCode = button.getAttribute("data-channel-code");
|
|
try {
|
|
await createOrder(channelCode);
|
|
} catch (error) {
|
|
state.pendingChannelCode = "";
|
|
renderChannels();
|
|
setStatus(extractErrorMessage(error));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function selectCountryByProfile() {
|
|
const userCountryCode = state.userProfile?.countryCode;
|
|
if (!userCountryCode) {
|
|
return false;
|
|
}
|
|
|
|
const matched = state.countries.find((item) =>
|
|
getCountryLabel(item).toUpperCase().includes(userCountryCode.toUpperCase())
|
|
);
|
|
|
|
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;
|
|
renderProducts();
|
|
renderChannels();
|
|
throw new Error("No supported country");
|
|
}
|
|
|
|
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(currentCountryId())}&type=${encodeURIComponent(
|
|
state.productType
|
|
)}®ionId=${encodeURIComponent(currentRegionId())}`
|
|
);
|
|
|
|
state.commodityCard = commodityResp?.body || {};
|
|
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 (!currentUserId()) {
|
|
throw new Error("Account not available");
|
|
}
|
|
|
|
state.pendingChannelCode = channelCode;
|
|
setStatus("");
|
|
renderChannels();
|
|
|
|
const payload = {
|
|
applicationId: state.applicationId,
|
|
goodsId: state.selectedGoodsId,
|
|
payCountryId: currentCountryId(),
|
|
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() {
|
|
renderProducts();
|
|
renderChannels();
|
|
|
|
try {
|
|
await reloadPageData();
|
|
} catch (error) {
|
|
setStatus(extractErrorMessage(error));
|
|
}
|
|
}
|
|
|
|
init();
|