hyapp-h5/recharge/index.html
2026-06-26 10:14:56 +08:00

2893 lines
118 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
/>
<title>Recharge Center</title>
<link rel="stylesheet" href="../common/theme.css" />
<style>
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
html,
body {
width: 100%;
min-height: 100%;
margin: 0;
background: var(--hy-theme-bg);
color: var(--hy-theme-text);
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Arial,
sans-serif;
}
body {
overflow-x: hidden;
--recharge-focus-ring: var(--hy-theme-focus-ring);
--recharge-role-shadow: var(--hy-theme-role-shadow);
--recharge-modal-shadow: var(--hy-theme-modal-shadow);
--recharge-checkout-shadow: var(--hy-theme-checkout-shadow);
--recharge-radius-tiny: var(--hy-theme-radius-tiny);
--recharge-radius-icon: var(--hy-theme-radius-icon);
--recharge-radius-small: var(--hy-theme-radius-small);
--recharge-radius-control: var(--hy-theme-radius-control);
--recharge-radius-card: var(--hy-theme-radius-card);
--recharge-radius-language: var(--hy-theme-radius-language);
--recharge-radius-modal: var(--hy-theme-radius-modal);
--recharge-radius-pill: var(--hy-theme-radius-pill);
--recharge-radius-circle: var(--hy-theme-radius-circle);
}
button,
input {
font: inherit;
}
button {
border: 0;
cursor: pointer;
}
.page {
width: min(100%, 520px);
min-height: 100vh;
margin: 0 auto;
padding: max(14px, env(safe-area-inset-top)) 14px
calc(110px + env(safe-area-inset-bottom));
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-height: 48px;
margin-bottom: 10px;
}
.topbar-copy {
min-width: 0;
}
.topbar h1,
.topbar p {
margin: 0;
}
.topbar h1 {
font-size: 24px;
line-height: 1.18;
letter-spacing: 0;
}
.topbar p {
margin-top: 3px;
color: var(--hy-theme-muted);
font-size: 13px;
line-height: 1.35;
}
.language-switcher {
position: relative;
flex: 0 0 auto;
z-index: 10;
}
.language-button {
min-width: 56px;
height: 34px;
padding: 0 12px;
border: 1px solid var(--hy-theme-line);
border-radius: var(--recharge-radius-language);
background: var(--hy-theme-surface);
color: var(--hy-theme-text);
font-weight: 800;
box-shadow: var(--hy-theme-shadow);
}
.language-menu {
position: absolute;
right: 0;
top: 40px;
width: 128px;
padding: 6px;
border: 1px solid var(--hy-theme-line);
border-radius: var(--recharge-radius-card);
background: var(--hy-theme-surface);
box-shadow: var(--hy-theme-shadow);
}
.language-menu button {
width: 100%;
min-height: 34px;
border-radius: var(--recharge-radius-small);
background: transparent;
color: var(--hy-theme-text);
text-align: left;
}
[dir='rtl'] .language-menu button {
text-align: right;
}
.language-menu button.is-active {
background: var(--hy-theme-primary-soft);
color: var(--hy-theme-primary-deep);
font-weight: 800;
}
.section {
margin-top: 10px;
padding: 14px;
border: 1px solid var(--hy-theme-line);
border-radius: var(--recharge-radius-card);
background: var(--hy-theme-surface);
box-shadow: var(--hy-theme-shadow);
}
.view {
display: none;
}
.view.is-active {
display: block;
}
.section-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.section-title h2 {
margin: 0;
font-size: 17px;
letter-spacing: 0;
}
.section-title span {
color: var(--hy-theme-muted);
font-size: 12px;
font-weight: 700;
}
.account-search {
display: grid;
gap: 8px;
grid-template-columns: minmax(0, 1fr) auto;
}
.input {
width: 100%;
height: 46px;
padding: 0 14px;
border: 1px solid var(--hy-theme-line);
border-radius: var(--recharge-radius-control);
background: var(--hy-theme-surface-soft);
color: var(--hy-theme-text);
outline: none;
}
.input:focus {
border-color: var(--hy-theme-primary-strong);
box-shadow: 0 0 0 3px var(--recharge-focus-ring);
}
.primary-button,
.secondary-button {
display: inline-flex;
min-height: 46px;
align-items: center;
justify-content: center;
padding: 0 16px;
border-radius: var(--recharge-radius-control);
font-weight: 850;
letter-spacing: 0;
}
.primary-button {
background: var(--hy-theme-button);
color: #fff;
box-shadow: var(--hy-theme-button-shadow);
}
.secondary-button {
border: 1px solid var(--hy-theme-line);
background: var(--hy-theme-surface-soft);
color: var(--hy-theme-primary-deep);
}
.primary-button:disabled,
.secondary-button:disabled {
background: var(--hy-theme-button-disabled);
box-shadow: var(--hy-theme-button-disabled-shadow);
color: var(--hy-theme-button-disabled-text);
cursor: default;
}
.account-card {
display: none;
align-items: center;
gap: 10px;
padding: 10px;
border-radius: var(--recharge-radius-control);
background: var(--hy-theme-primary-soft);
}
.account-card.is-visible {
display: flex;
}
.avatar {
width: 44px;
height: 44px;
flex: 0 0 auto;
overflow: hidden;
border-radius: var(--recharge-radius-card);
background: var(--hy-theme-primary);
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar span {
display: grid;
width: 100%;
height: 100%;
place-items: center;
color: var(--hy-theme-primary-deep);
font-size: 22px;
font-weight: 900;
}
.account-main {
min-width: 0;
flex: 1;
}
.account-name-row {
display: flex;
min-width: 0;
align-items: center;
gap: 6px;
}
.account-name,
.account-meta {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.account-name {
min-width: 0;
font-size: 15px;
font-weight: 850;
}
.account-meta {
margin-top: 4px;
color: var(--hy-theme-muted);
font-size: 13px;
}
.role-pill {
flex: 0 0 auto;
max-width: 112px;
padding: 4px 8px;
border-radius: var(--recharge-radius-pill);
border: 1px solid rgba(255, 255, 255, 0.62);
background: rgba(255, 255, 255, 0.48);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.58),
var(--recharge-role-shadow);
color: var(--hy-theme-primary-deep);
font-size: 11px;
font-weight: 850;
line-height: 1.15;
overflow: hidden;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.change-account {
min-width: 58px;
min-height: 34px;
padding: 0 10px;
border-radius: var(--recharge-radius-pill);
font-size: 12px;
}
.grid {
display: grid;
gap: 8px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.choice {
position: relative;
min-height: 72px;
padding: 12px;
border: 1px solid var(--hy-theme-line);
border-radius: var(--recharge-radius-control);
background: var(--hy-theme-surface-soft);
color: var(--hy-theme-text);
text-align: left;
}
.choice.is-selected {
border-color: var(--hy-theme-primary-strong);
background: var(--hy-theme-primary-soft);
box-shadow: inset 0 0 0 1px var(--hy-theme-primary-strong);
}
.choice strong,
.choice span {
display: block;
}
.choice strong {
font-size: 16px;
line-height: 1.2;
}
.choice span {
margin-top: 5px;
color: var(--hy-theme-muted);
font-size: 13px;
line-height: 1.35;
}
.method-list {
display: grid;
gap: 8px;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.method {
display: flex;
min-height: 58px;
align-items: center;
gap: 8px;
padding: 9px;
border: 1px solid var(--hy-theme-line);
border-radius: var(--recharge-radius-control);
background: var(--hy-theme-surface-soft);
color: var(--hy-theme-text);
text-align: left;
}
.method.is-selected {
border-color: var(--hy-theme-primary-strong);
background: var(--hy-theme-primary-soft);
}
.method-icon {
display: grid;
width: 38px;
height: 38px;
flex: 0 0 auto;
place-items: center;
overflow: hidden;
border-radius: var(--recharge-radius-icon);
background: var(--hy-theme-surface);
color: var(--hy-theme-primary-deep);
font-size: 11px;
font-weight: 900;
}
.method-logo {
display: block;
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.method-copy {
min-width: 0;
}
.method-name,
.method-amount {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.method-name {
color: var(--hy-theme-text);
font-size: 12px;
font-weight: 850;
line-height: 1.25;
}
.method-amount {
margin-top: 3px;
color: var(--hy-theme-primary-deep);
font-size: 11px;
font-weight: 850;
line-height: 1.25;
}
.order-panel {
display: none;
gap: 12px;
}
.order-panel.is-visible {
display: grid;
}
.order-row {
display: grid;
min-width: 0;
gap: 4px;
color: var(--hy-theme-muted);
font-size: 12px;
}
.order-row strong {
min-width: 0;
overflow: hidden;
color: var(--hy-theme-text);
text-overflow: ellipsis;
white-space: nowrap;
}
.modal-close {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 38px;
min-width: 38px;
min-height: 38px;
padding: 0;
border-radius: var(--recharge-radius-circle);
font-size: 0;
line-height: 1;
}
.modal-close::before,
.modal-close::after {
content: '';
position: absolute;
width: 16px;
height: 2px;
border-radius: var(--recharge-radius-pill);
background: currentColor;
}
.modal-close::before {
transform: rotate(45deg);
}
.modal-close::after {
transform: rotate(-45deg);
}
.payment-summary {
display: grid;
gap: 8px;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
align-items: stretch;
}
.summary-pill {
display: grid;
min-width: 78px;
align-content: center;
gap: 4px;
padding: 9px 10px;
border: 1px solid var(--hy-theme-line);
border-radius: var(--recharge-radius-control);
background: var(--hy-theme-surface-soft);
}
.summary-pill span {
color: var(--hy-theme-muted);
font-size: 11px;
font-weight: 750;
line-height: 1.15;
}
.summary-pill strong {
overflow: hidden;
color: var(--hy-theme-text);
font-size: 13px;
font-weight: 900;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.summary-pill.order-id-pill {
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
column-gap: 8px;
}
.summary-pill.order-id-pill span {
grid-column: 1 / -1;
}
.copy-icon-button {
position: relative;
width: 30px;
min-width: 30px;
min-height: 30px;
padding: 0;
border: 1px solid var(--hy-theme-line);
border-radius: var(--recharge-radius-icon);
background: var(--hy-theme-surface);
color: var(--hy-theme-primary-deep);
}
.copy-icon-button::before,
.copy-icon-button::after {
content: '';
position: absolute;
width: 12px;
height: 14px;
border: 2px solid currentColor;
border-radius: var(--recharge-radius-tiny);
}
.copy-icon-button::before {
right: 8px;
top: 7px;
background: var(--hy-theme-surface);
}
.copy-icon-button::after {
right: 12px;
top: 10px;
opacity: 0.65;
}
.status-pill {
display: inline-flex;
align-items: center;
justify-content: center;
align-self: center;
gap: 6px;
color: var(--hy-theme-primary-deep);
font-size: 13px;
font-weight: 900;
white-space: nowrap;
}
.status-pill::before {
content: '';
width: 8px;
height: 8px;
border-radius: var(--recharge-radius-circle);
background: var(--hy-theme-primary-deep);
box-shadow: 0 0 0 4px var(--hy-theme-primary-soft);
}
.address-box {
display: grid;
gap: 8px;
padding: 12px;
border-radius: var(--recharge-radius-control);
background: var(--hy-theme-primary-soft);
}
#usdt-panel {
display: grid;
gap: 12px;
}
.address-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.address-head span {
color: var(--hy-theme-text);
font-size: 13px;
font-weight: 850;
}
.copy-address-button {
min-height: 34px;
padding: 0 12px;
border-radius: var(--recharge-radius-small);
background: var(--hy-theme-surface);
font-size: 12px;
}
.address-value {
overflow-wrap: anywhere;
color: var(--hy-theme-text);
font-family:
ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 12px;
font-weight: 850;
line-height: 1.45;
}
.tx-form {
display: grid;
gap: 8px;
}
.modal-backdrop {
position: fixed;
inset: 0;
z-index: 100;
display: grid;
align-items: end;
padding: 16px;
background: rgba(34, 29, 46, 0.5);
}
.modal {
width: min(100%, 520px);
max-height: min(82vh, 680px);
margin: 0 auto;
overflow: auto;
border: 1px solid var(--hy-theme-line);
border-radius: var(--recharge-radius-modal);
background: var(--hy-theme-surface);
box-shadow: var(--recharge-modal-shadow);
}
.modal-header {
position: sticky;
top: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 18px 16px 14px;
border-bottom: 1px solid var(--hy-theme-line);
background: var(--hy-theme-surface);
}
.modal-header h2 {
margin: 0;
font-size: 18px;
letter-spacing: 0;
}
.modal-header > div {
min-width: 0;
flex: 1;
}
.modal-body {
padding: 12px 16px 16px;
}
.modal-actions {
display: grid;
gap: 10px;
margin-top: 12px;
}
.empty,
.status-line {
color: var(--hy-theme-muted);
font-size: 14px;
line-height: 1.5;
}
.status-line {
margin-top: 10px;
}
.checkout-bar {
position: fixed;
right: 0;
bottom: 0;
left: 0;
z-index: 30;
padding: 10px 14px calc(10px + env(safe-area-inset-bottom));
border-top: 1px solid var(--hy-theme-line);
background: rgba(255, 255, 255, 0.94);
box-shadow: var(--recharge-checkout-shadow);
backdrop-filter: blur(12px);
}
.checkout-inner {
display: flex;
width: min(100%, 492px);
align-items: center;
justify-content: space-between;
gap: 10px;
margin: 0 auto;
}
.checkout-summary {
min-width: 0;
color: var(--hy-theme-muted);
font-size: 12px;
line-height: 1.25;
}
.checkout-summary strong,
.checkout-summary span {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.checkout-summary strong {
color: var(--hy-theme-text);
font-size: 14px;
font-weight: 900;
}
.checkout-inner .primary-button {
min-width: 118px;
flex: 0 0 auto;
}
.hidden {
display: none !important;
}
@media (max-width: 390px) {
.page {
padding-right: 12px;
padding-left: 12px;
}
.account-search {
grid-template-columns: 1fr;
}
.choice {
min-height: 78px;
padding: 10px;
}
.choice strong {
font-size: 15px;
}
.choice span {
font-size: 12px;
}
.method-list {
gap: 8px;
}
.method {
min-height: 56px;
padding: 8px;
}
.checkout-inner .primary-button {
min-width: 104px;
padding: 0 12px;
}
}
</style>
</head>
<body>
<main class="page" aria-labelledby="page-title">
<div id="package-view" class="view is-active">
<header class="topbar">
<div class="topbar-copy">
<h1 id="page-title" data-i18n="recharge.pageTitle">
Recharge Center
</h1>
<p data-i18n="recharge.pageSubtitle">
Choose a coin package and pay.
</p>
</div>
<div class="language-switcher">
<button
class="language-button"
type="button"
data-language-toggle
data-i18n-aria="recharge.changeLanguage"
aria-expanded="false"
>
<span data-current-lang>EN</span>
</button>
<div class="language-menu" data-language-menu hidden>
<button type="button" data-lang-option="en">
English
</button>
<button type="button" data-lang-option="zh">
中文
</button>
<button type="button" data-lang-option="ar">
العربية
</button>
<button type="button" data-lang-option="tr">
Türkçe
</button>
<button type="button" data-lang-option="es">
Español
</button>
<button type="button" data-lang-option="id">
Indonesia
</button>
</div>
</div>
</header>
<section class="section" id="account-section">
<div class="section-title">
<h2 data-i18n="recharge.accountTitle">Account</h2>
<span id="auth-mode"></span>
</div>
<div id="account-search" class="account-search">
<input
id="display-user-id"
class="input"
autocomplete="off"
inputmode="numeric"
data-i18n-placeholder="recharge.userIdPlaceholder"
placeholder="Enter user ID"
/>
<button
id="search-account"
class="primary-button"
type="button"
data-i18n="recharge.searchAccount"
>
Confirm
</button>
</div>
<div id="account-card" class="account-card">
<div class="avatar" id="account-avatar"></div>
<div class="account-main">
<div class="account-name-row">
<div class="account-name" id="account-name">
-
</div>
<div class="role-pill" id="account-role">-</div>
</div>
<div class="account-meta" id="account-meta">-</div>
</div>
<button
id="change-account"
class="secondary-button change-account"
type="button"
data-i18n="recharge.changeAccount"
>
Change
</button>
</div>
<div
id="account-status"
class="status-line"
data-i18n="recharge.accountHint"
>
Confirm the account before selecting a package.
</div>
</section>
<section class="section" id="product-section">
<div class="section-title">
<h2 data-i18n="recharge.productTitle">Package</h2>
<span id="product-count"></span>
</div>
<div id="product-list" class="grid"></div>
<div
id="product-empty"
class="empty"
data-i18n="recharge.productEmpty"
>
No package is available for this account.
</div>
</section>
<section class="section" id="payment-section">
<div class="section-title">
<h2 data-i18n="recharge.paymentTitle">Payment</h2>
<span id="method-count"></span>
</div>
<div id="method-list" class="method-list"></div>
<div
id="method-empty"
class="empty"
data-i18n="recharge.noPaymentMethods"
>
No payment method is available.
</div>
</section>
</div>
<div id="checkout-bar" class="checkout-bar hidden">
<div class="checkout-inner">
<div class="checkout-summary">
<strong id="checkout-coins">-</strong>
<span id="checkout-amount">-</span>
</div>
<button
id="create-order"
class="primary-button"
type="button"
disabled
data-i18n="recharge.createOrder"
>
Pay now
</button>
</div>
</div>
<div
id="order-modal"
class="modal-backdrop hidden"
role="dialog"
aria-modal="true"
>
<div class="modal">
<div class="modal-header">
<div>
<h2
id="order-modal-title"
data-i18n="recharge.paymentOperation"
>
Payment operation
</h2>
</div>
<strong id="order-status" class="status-pill">-</strong>
<button
id="close-order-modal"
class="secondary-button modal-close"
type="button"
aria-label="Close"
data-i18n-aria="recharge.close"
></button>
</div>
<div class="modal-body">
<section id="order-panel" class="order-panel">
<div class="payment-summary">
<div class="summary-pill">
<span data-i18n="recharge.amount"
>Amount</span
>
<strong id="order-amount">-</strong>
</div>
<div class="summary-pill order-id-pill">
<span data-i18n="recharge.orderId"
>Order ID</span
>
<strong id="order-id">-</strong>
<button
id="copy-order-id"
class="copy-icon-button"
type="button"
aria-label="Copy order ID"
data-i18n-aria="recharge.copyOrderId"
></button>
</div>
</div>
<div id="usdt-panel" class="hidden">
<div class="address-box">
<div class="address-head">
<span data-i18n="recharge.usdtAddress">
USDT-TRC20 address
</span>
<button
id="copy-address"
class="secondary-button copy-address-button"
type="button"
data-i18n="recharge.copyAddress"
>
Copy address
</button>
</div>
<div
class="address-value"
id="receive-address"
>
-
</div>
</div>
<form id="tx-form" class="tx-form">
<input
id="tx-hash"
class="input"
autocomplete="off"
data-i18n-placeholder="recharge.txHashPlaceholder"
placeholder="Enter TXID after payment"
/>
<button
id="submit-tx"
class="primary-button"
type="submit"
data-i18n="recharge.submitTx"
>
Submit TXID
</button>
</form>
</div>
<div
id="mifapay-panel"
class="modal-actions hidden"
>
<button
id="open-payment-url"
class="primary-button"
type="button"
data-i18n="recharge.openPayment"
>
Open payment
</button>
</div>
<div id="order-message" class="status-line"></div>
</section>
</div>
</div>
</div>
</main>
<script src="../common/api.js"></script>
<script src="../common/params.js"></script>
<script src="../common/i18n.js"></script>
<script src="../common/toast.js"></script>
<script>
(function () {
var PENDING_EXTERNAL_ORDER_KEY =
'hyapp_recharge_pending_external_order';
var LEGACY_PENDING_MIFAPAY_ORDER_KEY =
'hyapp_recharge_pending_mifapay_order';
var RETURN_MIFAPAY_ORDER_QUERY_KEY = 'hyapp_order_id';
var PENDING_EXTERNAL_MAX_AGE_MS = 24 * 60 * 60 * 1000;
var state = {
context: null,
options: null,
appConfig: null,
selectedPayCountryID: '',
selectedCountryCode: '',
selectedCurrencyCode: '',
selectedProductID: 0,
selectedMethodKey: '',
order: null,
orderModalOpen: false,
pollingTimer: 0,
loading: false,
orderNoticeKey: '',
confirmingExternalPayment: false,
lastExternalResumeAtMS: 0,
};
var nodes = {};
// 页面只依赖公共 api/params/i18n/themetoken、app_code、env 都交给 common 层解析。
// 初始化阶段只决定入口模式,带 token 直接查本人,不带 token 才允许输入 display_user_id。
function init() {
nodes = {
packageView: document.getElementById('package-view'),
accountSearch:
document.getElementById('account-search'),
accountCard: document.getElementById('account-card'),
accountAvatar:
document.getElementById('account-avatar'),
accountName: document.getElementById('account-name'),
accountMeta: document.getElementById('account-meta'),
accountRole: document.getElementById('account-role'),
accountStatus:
document.getElementById('account-status'),
authMode: document.getElementById('auth-mode'),
changeAccount:
document.getElementById('change-account'),
displayUserID:
document.getElementById('display-user-id'),
searchAccount:
document.getElementById('search-account'),
productList: document.getElementById('product-list'),
productSection:
document.getElementById('product-section'),
productEmpty: document.getElementById('product-empty'),
productCount: document.getElementById('product-count'),
paymentSection:
document.getElementById('payment-section'),
methodList: document.getElementById('method-list'),
methodEmpty: document.getElementById('method-empty'),
methodCount: document.getElementById('method-count'),
checkoutBar: document.getElementById('checkout-bar'),
checkoutCoins:
document.getElementById('checkout-coins'),
checkoutAmount:
document.getElementById('checkout-amount'),
createOrder: document.getElementById('create-order'),
orderModal: document.getElementById('order-modal'),
closeOrderModal:
document.getElementById('close-order-modal'),
orderPanel: document.getElementById('order-panel'),
orderModalTitle:
document.getElementById('order-modal-title'),
orderAmount: document.getElementById('order-amount'),
orderID: document.getElementById('order-id'),
copyOrderID: document.getElementById('copy-order-id'),
orderStatus: document.getElementById('order-status'),
orderMessage: document.getElementById('order-message'),
usdtPanel: document.getElementById('usdt-panel'),
mifapayPanel: document.getElementById('mifapay-panel'),
openPaymentURL:
document.getElementById('open-payment-url'),
receiveAddress:
document.getElementById('receive-address'),
copyAddress: document.getElementById('copy-address'),
txForm: document.getElementById('tx-form'),
txHash: document.getElementById('tx-hash'),
submitTx: document.getElementById('submit-tx'),
};
bindEvents();
var current = window.HyAppParams
? window.HyAppParams.parse()
: { raw: currentRawParams() };
var raw = current.raw || currentRawParams();
var pendingOrder = readPendingExternalOrder();
var displayUserID =
raw.get('display_user_id') ||
(pendingOrder && pendingOrder.display_user_id) ||
'';
nodes.displayUserID.value = displayUserID;
var configReady =
window.HyAppAPI && window.HyAppAPI.loadAppConfig
? window.HyAppAPI.loadAppConfig('recharge')
: Promise.resolve(null);
configReady
.then(function (config) {
state.appConfig =
config ||
(window.HyAppAPI && window.HyAppAPI.getAppConfig
? window.HyAppAPI.getAppConfig()
: null);
})
.finally(function () {
continueInit(current, displayUserID);
});
}
function continueInit(current, displayUserID) {
if (current.token) {
// token 模式必须隐藏账号输入,目标用户只能由服务端鉴权结果决定,不能让前端改充值对象。
nodes.accountSearch.classList.add('hidden');
loadContext('');
} else if (displayUserID) {
// 链接预填 display_user_id 时自动确认一次,适合客服或分享充值链接直接带目标账号。
loadContext(displayUserID);
} else {
render();
}
}
// 所有用户动作集中绑定,异步入口统一进入 load/create/submit避免多个按钮各自拼接口参数。
function bindEvents() {
nodes.searchAccount.addEventListener('click', function () {
loadContext(nodes.displayUserID.value);
});
nodes.changeAccount.addEventListener(
'click',
changeAccount
);
nodes.createOrder.addEventListener('click', createOrder);
nodes.closeOrderModal.addEventListener(
'click',
closeOrderModal
);
nodes.openPaymentURL.addEventListener('click', function () {
if (state.order && state.order.pay_url) {
window.location.href = state.order.pay_url;
}
});
nodes.copyAddress.addEventListener('click', copyAddress);
nodes.copyOrderID.addEventListener('click', copyOrderID);
nodes.txForm.addEventListener('submit', submitTx);
window.addEventListener('pageshow', resumeExternalOrder);
window.addEventListener('focus', resumeExternalOrder);
document.addEventListener(
'visibilitychange',
resumeVisibleExternalOrder
);
window.addEventListener('hyapp:i18n-ready', function () {
document.title = t(
'recharge.pageTitle',
'Recharge Center'
);
render();
});
}
// 账号确认是后续商品、支付方式、钱包入账目标的唯一前置事实。
// 无 token 时必须提供 display_user_id有 token 时 display_user_id 置空,后端只认 token 用户。
function loadContext(displayUserID) {
displayUserID = String(displayUserID || '').trim();
if (!window.HyAppAPI.getAccessToken() && !displayUserID) {
toast(t('recharge.accountRequired', 'Enter user ID'));
return;
}
setBusy(true);
setStatus(
nodes.accountStatus,
t('recharge.loading', 'Loading...')
);
window.HyAppAPI.recharge
.context(displayUserID)
.then(function (context) {
// context 只返回安全账号信息和 audience_type前端不读取手机号、邮箱、余额等敏感字段。
state.context = context;
var selectedPayCountry =
selectPayCountryFromConfig(context);
if (
configuredMifaPayCountries().length &&
!selectedPayCountry
) {
return Promise.resolve(
emptyOptionsForSelectedCountry(context)
);
}
return window.HyAppAPI.recharge.options(
context.auth_mode === 'token'
? ''
: context.account.display_user_id,
{
pay_country_id: state.selectedPayCountryID,
country_code: state.selectedCountryCode,
region_id:
context.region_id || context.regionId,
user_id: currentAccountUserID(),
commodity_type:
currentAccountCommodityType(),
}
);
})
.then(function (options) {
// 商品和支付方式必须跟随已确认账号重新加载,切换账号后清空旧选择和旧订单。
applyResolvedPayCountry(options);
state.options = options;
state.selectedProductID = 0;
state.selectedMethodKey = '';
state.order = null;
state.orderModalOpen = false;
render();
restorePendingExternalOrder();
})
.catch(function (error) {
toast(
error.message ||
t('recharge.loadFailed', 'Load failed')
);
setStatus(
nodes.accountStatus,
t('recharge.loadFailed', 'Load failed')
);
})
.finally(function () {
setBusy(false);
});
}
// render 只消费 state不发请求所有接口副作用集中在动作函数里方便排查状态流转。
function render() {
normalizeSelections();
renderAccount();
renderProducts();
renderMethods();
renderCheckout();
renderOrder();
}
// 账号卡只展示后端允许公开的账号确认信息;受众文案直接来自 audience_type。
function renderAccount() {
var account = state.context && state.context.account;
nodes.authMode.textContent = state.context
? state.context.auth_mode === 'token'
? 'Token'
: 'ID'
: '';
nodes.accountCard.classList.toggle('is-visible', !!account);
nodes.accountSearch.classList.toggle(
'hidden',
!!account ||
(state.context &&
state.context.auth_mode === 'token')
);
nodes.changeAccount.classList.toggle(
'hidden',
!account ||
(state.context &&
state.context.auth_mode === 'token')
);
nodes.accountStatus.classList.toggle('hidden', !!account);
if (!account) return;
nodes.accountAvatar.innerHTML = account.avatar
? '<img src="' +
escapeAttr(account.avatar) +
'" alt="" />'
: '<span>' +
escapeHTML(firstLetter(account.username)) +
'</span>';
nodes.accountName.textContent =
account.username ||
account.display_user_id ||
t('recharge.accountConfirmed', 'Confirmed account');
nodes.accountMeta.textContent = accountMetaText(account);
nodes.accountRole.textContent = isFreightAccount(account)
? t('recharge.coinSeller', 'Coin seller')
: t('recharge.ordinaryUser', 'User');
setStatus(
nodes.accountStatus,
t('recharge.accountConfirmed', 'Account confirmed')
);
}
// 更换账号必须先清空商品、支付方式和旧订单,防止用户在输入新 ID 时误用上一个账号的商品快照下单。
function changeAccount() {
var previousID = currentAccountDisplayUserID();
state.context = null;
state.options = null;
state.selectedPayCountryID = '';
state.selectedCountryCode = '';
state.selectedCurrencyCode = '';
state.selectedProductID = 0;
state.selectedMethodKey = '';
state.order = null;
state.orderModalOpen = false;
nodes.displayUserID.value = previousID;
setStatus(
nodes.accountStatus,
t(
'recharge.accountHint',
'Confirm the account before selecting a package.'
)
);
render();
nodes.displayUserID.focus();
}
// 商品列表由后端按 normal / coin_seller 和用户区域过滤,前端只负责单选交互。
function renderProducts() {
var products =
(state.options && state.options.products) || [];
nodes.productList.innerHTML = '';
nodes.productSection.classList.toggle(
'hidden',
!state.context
);
nodes.productCount.textContent = products.length
? String(products.length)
: '';
nodes.productEmpty.classList.toggle(
'hidden',
products.length > 0
);
products.forEach(function (product) {
var currentProductID = productID(product);
var button = document.createElement('button');
button.type = 'button';
button.className =
'choice' +
(currentProductID === state.selectedProductID
? ' is-selected'
: '');
button.innerHTML =
'<strong>' +
escapeHTML(formatCoins(product.coin_amount)) +
' ' +
escapeHTML(t('recharge.coins', 'coins')) +
'</strong><span>' +
escapeHTML(formatUSD(product)) +
'</span>';
button.addEventListener('click', function () {
state.selectedProductID = currentProductID;
// 档位、支付方式和订单必须是一组同源快照;切档位时清空旧支付方式和旧订单,防止旧金额被带入新订单。
state.selectedMethodKey = '';
state.order = null;
state.orderModalOpen = false;
render();
});
nodes.productList.appendChild(button);
});
}
// 支付方式展示由当前商品驱动USDT 用订单美元金额,三方本地支付用后台汇率换算本币金额。
function renderMethods() {
var methods = availableMethods();
var hasProduct = !!selectedProduct();
nodes.methodList.innerHTML = '';
nodes.methodCount.textContent = methods.length
? String(methods.length)
: '';
nodes.paymentSection.classList.toggle(
'hidden',
!hasProduct
);
nodes.methodEmpty.classList.toggle(
'hidden',
methods.length > 0
);
nodes.methodEmpty.textContent = state.selectedProductID
? t('recharge.noPaymentMethods', 'No payment method')
: t(
'recharge.selectProductFirst',
'Select a package first'
);
methods.forEach(function (method) {
var button = document.createElement('button');
button.type = 'button';
button.className =
'method' +
(method.key === state.selectedMethodKey
? ' is-selected'
: '');
button.innerHTML =
'<span class="method-icon">' +
methodIconHTML(method) +
'</span><span class="method-copy"><span class="method-name">' +
escapeHTML(method.name) +
'</span><span class="method-amount">' +
escapeHTML(method.amount) +
'</span></span>';
button.addEventListener('click', function () {
state.selectedMethodKey = method.key;
render();
});
nodes.methodList.appendChild(button);
});
nodes.createOrder.disabled =
!state.selectedProductID ||
!state.selectedMethodKey ||
state.loading;
}
// 底部付款栏只在已选档位后出现,避免页面首屏被无效按钮占位;按钮是否可点继续由支付方式和 loading 状态控制。
function renderCheckout() {
var product = selectedProduct();
nodes.checkoutBar.classList.toggle('hidden', !product);
if (!product) {
nodes.checkoutCoins.textContent = '-';
nodes.checkoutAmount.textContent = '-';
return;
}
nodes.checkoutCoins.textContent =
formatCoins(product.coin_amount) +
' ' +
t('recharge.coins', 'coins');
nodes.checkoutAmount.textContent = formatUSD(product);
nodes.createOrder.disabled =
state.loading || !state.selectedMethodKey;
}
// 订单弹窗是支付中的操作入口;终态由服务端查询结果驱动,成功后关闭弹窗,避免继续展示打开支付按钮。
// 失败仍保留弹窗展示失败原因,方便用户看到三方拒单或本地校验失败的信息。
function renderOrder() {
var order = state.order;
nodes.orderModal.classList.toggle(
'hidden',
!order || !state.orderModalOpen
);
nodes.orderPanel.classList.toggle('is-visible', !!order);
if (!order) return;
var isUSDT = order.provider_code === 'usdt_trc20';
nodes.orderModalTitle.textContent = isUSDT
? t('recharge.usdtPayment', 'USDT payment')
: t('recharge.paymentOperation', 'Payment operation');
nodes.orderAmount.textContent = orderAmountText(order);
nodes.orderID.textContent = shortOrderID(order.order_id);
nodes.orderID.title = order.order_id || '';
nodes.orderStatus.textContent = statusLabel(order.status);
nodes.usdtPanel.classList.toggle('hidden', !isUSDT);
nodes.mifapayPanel.classList.toggle(
'hidden',
!isRedirectPaymentOrder(order)
);
if (order.provider_code === 'usdt_trc20') {
nodes.receiveAddress.textContent =
order.receive_address ||
(state.options &&
state.options.usdt_trc20_address) ||
'-';
}
if (order.status === 'credited') {
setStatus(
nodes.orderMessage,
t('recharge.paidSuccess', 'Payment completed')
);
} else if (order.status === 'failed') {
setStatus(
nodes.orderMessage,
order.failure_reason ||
t('recharge.orderFailed', 'Order failed')
);
} else if (isRedirectPaymentProvider(order.provider_code)) {
setStatus(
nodes.orderMessage,
state.confirmingExternalPayment
? t(
'recharge.confirmingPayment',
'Confirming payment status...'
)
: t(
'recharge.payOpenHint',
'Open the payment page and finish the payment.'
)
);
} else {
setStatus(nodes.orderMessage, '');
}
}
// 可用支付方式只在选中商品后生成,避免用户先选支付方式再切商品造成金额不一致。
// 三方本地支付方式必须是 activeUSDT 必须同时有开关和收款地址,缺地址时不展示。
function availableMethods() {
var product = selectedProduct();
if (!product) return [];
var methods = [];
if (
state.options &&
state.options.usdt_trc20_enabled &&
state.options.usdt_trc20_address
) {
methods.push({
key: 'usdt_trc20',
icon: 'USDT',
name: t('recharge.usdtTrc20', 'USDT-TRC20'),
meta: t('recharge.usdtNetwork', 'TRON network'),
amount: formatUSD(product),
providerCode: 'usdt_trc20',
});
}
((state.options && state.options.payment_methods) || [])
.filter(function (method) {
// 正常国家命中时继续隐藏 GLOBAL如果用户国家没有方式则后端商品仍按用户国家返回
// 前端按 JSON 全量兜底展示所有方式,避免用户被空支付方式卡住。
return (
method.status === 'active' &&
(method.fallback_all_countries ||
String(
method.country_code || ''
).toUpperCase() !== 'GLOBAL')
);
})
.forEach(function (method) {
methods.push({
key:
(method.provider_code || 'mifapay') +
':' +
method.method_id,
icon: method.currency_code || 'PAY',
name:
method.method_name ||
method.pay_way ||
t(
'recharge.thirdPartyPayment',
'Local payment'
),
meta: [
method.country_name || method.country_code,
method.pay_type,
]
.filter(Boolean)
.join(' · '),
logoURL: method.logo_url || '',
amount: convertedAmount(product, method),
providerCode: method.provider_code || 'mifapay',
methodID: method.method_id,
payCountryID:
method.pay_country_id ||
state.selectedPayCountryID,
countryCode:
method.country_code ||
state.selectedCountryCode,
currencyCode:
method.currency_code ||
state.selectedCurrencyCode,
payWay: method.pay_way,
payType: method.pay_type,
channelCode:
method.channel_code ||
[method.pay_way, method.pay_type]
.filter(Boolean)
.join('_'),
});
});
return methods;
}
// 支付卡片只保留 logo 和金额:有后台 logo_url 就显示真实渠道图,无图才用短币种文本兜底。
function methodIconHTML(method) {
if (method.logoURL) {
return (
'<img class="method-logo" src="' +
escapeAttr(method.logoURL) +
'" alt="" />'
);
}
return escapeHTML(method.icon);
}
// 创建订单时只提交 product_id、provider 和 method_id金额、金币、币商/普通钱包都由服务端订单快照决定。
// 三方支付创建成功后必须立刻跳收银台;本页保存 order_id支付完成 return 回来后继续轮询状态。
function createOrder() {
var product = selectedProduct();
var method = selectedMethod();
if (!product || !method) return;
setBusy(true);
nodes.createOrder.textContent = t(
'recharge.creating',
'Creating...'
);
window.HyAppAPI.recharge
.createOrder({
display_user_id: targetDisplayUserID(),
product_id: createOrderProductID(product),
user_id: currentAccountUserID(),
commodity_type: currentAccountCommodityType(),
pay_country_id:
method.payCountryID ||
state.selectedPayCountryID,
provider_code: method.providerCode,
payment_method_id: method.methodID || 0,
pay_way: method.payWay || '',
pay_type: method.payType || '',
channel_code: method.channelCode || '',
country_code:
method.countryCode || state.selectedCountryCode,
currency_code:
method.currencyCode ||
state.selectedCurrencyCode,
command_id: commandID(product, method),
return_url: externalPayReturnURL(),
})
.then(function (payload) {
state.order = payload.order || payload;
state.orderModalOpen = true;
renderOrder();
if (isTerminalOrder(state.order)) {
applyOrderTerminalSideEffects(state.order);
return;
}
startPolling();
if (isRedirectPaymentOrder(state.order)) {
savePendingExternalOrder(state.order);
window.location.href = state.order.pay_url;
}
})
.catch(function (error) {
toast(
error.message ||
t(
'recharge.orderCreateFailed',
'Create order failed'
)
);
})
.finally(function () {
setBusy(false);
nodes.createOrder.textContent = t(
'recharge.createOrder',
'Pay now'
);
renderMethods();
renderCheckout();
});
}
function closeOrderModal() {
state.orderModalOpen = false;
renderOrder();
}
// USDT 模式只收 tx_hash链上合约、to 地址、金额、确认数和重复 txid 都由后端校验。
function submitTx(event) {
event.preventDefault();
if (!state.order || !nodes.txHash.value.trim()) return;
setBusy(true);
nodes.submitTx.textContent = t(
'recharge.submitted',
'Submitting...'
);
window.HyAppAPI.recharge
.submitTx(state.order.order_id, {
display_user_id: targetDisplayUserID(),
tx_hash: nodes.txHash.value.trim(),
})
.then(function (payload) {
state.order = payload.order || payload;
toast(t('recharge.txSubmitted', 'TXID submitted'));
renderOrder();
startPolling();
})
.catch(function (error) {
toast(
error.message ||
t('recharge.loadFailed', 'Load failed')
);
})
.finally(function () {
setBusy(false);
nodes.submitTx.textContent = t(
'recharge.submitTx',
'Submit TXID'
);
});
}
// 轮询只在订单存在时启动credited/failed 是终态,终态后停止,避免 H5 后台持续打接口。
// 返回 H5 后必须立即查一次,不再等第一个 5 秒周期,否则用户会看到“已返回但无反馈”。
function startPolling() {
window.clearInterval(state.pollingTimer);
if (!state.order || !state.order.order_id) return;
if (isTerminalOrder(state.order)) {
applyOrderTerminalSideEffects(state.order);
return;
}
refreshOrderStatus();
state.pollingTimer = window.setInterval(function () {
if (!state.order || isTerminalOrder(state.order)) {
window.clearInterval(state.pollingTimer);
return;
}
refreshOrderStatus();
}, 5000);
}
function refreshOrderStatus() {
if (!state.order || !state.order.order_id) {
return Promise.resolve();
}
if (isTerminalOrder(state.order)) {
applyOrderTerminalSideEffects(state.order);
return Promise.resolve(state.order);
}
if (isRedirectPaymentProvider(state.order.provider_code)) {
state.confirmingExternalPayment = true;
state.orderModalOpen = true;
renderOrder();
}
return window.HyAppAPI.recharge
.getOrder(state.order.order_id, orderStatusUserID())
.then(function (payload) {
state.order = payload.order || payload;
renderOrder();
applyOrderTerminalSideEffects(state.order);
return state.order;
})
.catch(function () {});
}
function restorePendingExternalOrder() {
var pending = readPendingExternalOrder();
if (!pending) return;
if (
state.order &&
state.order.order_id &&
state.order.order_id !== pending.order_id
) {
return;
}
var accountDisplayUserID = currentAccountDisplayUserID();
if (
pending.display_user_id &&
accountDisplayUserID &&
pending.display_user_id !== accountDisplayUserID
) {
return;
}
if (
state.order &&
state.order.order_id === pending.order_id
) {
state.confirmingExternalPayment = true;
state.orderModalOpen = true;
renderOrder();
startPolling();
return;
}
state.confirmingExternalPayment = true;
window.HyAppAPI.recharge
.getOrder(
pending.order_id,
orderStatusUserID() || pending.display_user_id
)
.then(function (payload) {
state.order = payload.order || payload;
state.orderModalOpen = true;
renderOrder();
applyOrderTerminalSideEffects(state.order);
if (!isTerminalOrder(state.order)) {
startPolling();
}
})
.catch(function () {
clearPendingExternalOrder(pending.order_id);
});
}
function resumeVisibleExternalOrder() {
if (
document.visibilityState &&
document.visibilityState !== 'visible'
) {
return;
}
resumeExternalOrder();
}
function resumeExternalOrder() {
if (!state.context) return;
if (Date.now() - state.lastExternalResumeAtMS < 800) {
return;
}
state.lastExternalResumeAtMS = Date.now();
if (
state.order &&
isRedirectPaymentProvider(state.order.provider_code) &&
!isTerminalOrder(state.order)
) {
state.confirmingExternalPayment = true;
state.orderModalOpen = true;
renderOrder();
startPolling();
return;
}
restorePendingExternalOrder();
}
function applyOrderTerminalSideEffects(order) {
if (!isTerminalOrder(order)) {
return;
}
state.confirmingExternalPayment = false;
clearPendingExternalOrder(order.order_id);
clearReturnedMifaPayOrderQuery();
window.clearInterval(state.pollingTimer);
if (order.status === 'credited') {
state.orderModalOpen = false;
renderOrder();
}
var noticeKey = order.order_id + ':' + order.status;
if (state.orderNoticeKey === noticeKey) return;
state.orderNoticeKey = noticeKey;
if (order.status === 'credited') {
toast(t('recharge.paidSuccess', 'Payment completed'));
} else if (order.status === 'failed') {
toast(
order.failure_reason ||
t('recharge.orderFailed', 'Order failed')
);
}
}
function savePendingExternalOrder(order) {
if (!order || !order.order_id) return;
storageSet(
PENDING_EXTERNAL_ORDER_KEY,
JSON.stringify({
order_id: order.order_id,
display_user_id: currentAccountDisplayUserID(),
created_at_ms: Date.now(),
})
);
storageRemove(LEGACY_PENDING_MIFAPAY_ORDER_KEY);
}
function readPendingExternalOrder() {
var returned = readReturnedMifaPayOrder();
if (returned) return returned;
try {
var raw =
storageGet(PENDING_EXTERNAL_ORDER_KEY) ||
storageGet(LEGACY_PENDING_MIFAPAY_ORDER_KEY);
if (!raw) return null;
var parsed = JSON.parse(raw);
if (!parsed || !parsed.order_id) return null;
if (
Number(parsed.created_at_ms || 0) > 0 &&
Date.now() - Number(parsed.created_at_ms) >
PENDING_EXTERNAL_MAX_AGE_MS
) {
clearPendingExternalOrder(parsed.order_id);
return null;
}
return parsed;
} catch (error) {
clearPendingExternalOrder('');
return null;
}
}
function readReturnedMifaPayOrder() {
var raw = currentRawParams();
var orderID = String(
raw.get(RETURN_MIFAPAY_ORDER_QUERY_KEY) || ''
).trim();
if (!orderID) return null;
return {
order_id: orderID,
display_user_id:
raw.get('display_user_id') ||
currentAccountDisplayUserID(),
created_at_ms: Date.now(),
};
}
function clearPendingExternalOrder(orderID) {
var pending = null;
try {
pending = JSON.parse(
storageGet(PENDING_EXTERNAL_ORDER_KEY) ||
storageGet(LEGACY_PENDING_MIFAPAY_ORDER_KEY) ||
'null'
);
} catch (error) {
pending = null;
}
if (
!orderID ||
!pending ||
!pending.order_id ||
pending.order_id === orderID
) {
storageRemove(PENDING_EXTERNAL_ORDER_KEY);
storageRemove(LEGACY_PENDING_MIFAPAY_ORDER_KEY);
}
}
function clearReturnedMifaPayOrderQuery() {
try {
var url = new URL(window.location.href);
var changed = false;
if (
url.searchParams.has(RETURN_MIFAPAY_ORDER_QUERY_KEY)
) {
url.searchParams.delete(
RETURN_MIFAPAY_ORDER_QUERY_KEY
);
changed = true;
}
var hash = url.hash || '';
var hashQueryIndex = hash.indexOf('?');
if (hashQueryIndex >= 0) {
var hashPath = hash.slice(0, hashQueryIndex);
var hashParams = new URLSearchParams(
hash.slice(hashQueryIndex + 1)
);
if (
hashParams.has(RETURN_MIFAPAY_ORDER_QUERY_KEY)
) {
hashParams.delete(
RETURN_MIFAPAY_ORDER_QUERY_KEY
);
url.hash = hashParams.toString()
? hashPath + '?' + hashParams.toString()
: hashPath;
changed = true;
}
}
if (
window.HyAppParams &&
window.HyAppParams.current &&
window.HyAppParams.current.raw
) {
window.HyAppParams.current.raw.delete(
RETURN_MIFAPAY_ORDER_QUERY_KEY
);
}
if (
changed &&
window.history &&
window.history.replaceState
) {
window.history.replaceState(
window.history.state,
document.title,
url.toString()
);
}
} catch (error) {}
}
function storageGet(key) {
try {
return window.localStorage
? window.localStorage.getItem(key)
: '';
} catch (error) {
return '';
}
}
function storageSet(key, value) {
try {
if (window.localStorage) {
window.localStorage.setItem(key, value);
}
} catch (error) {}
}
function storageRemove(key) {
try {
if (window.localStorage) {
window.localStorage.removeItem(key);
}
} catch (error) {}
}
// 复制地址不改变订单状态,只是降低 USDT 转账出错率;浏览器不支持 clipboard 时走 textarea 兜底。
function copyAddress() {
var address = nodes.receiveAddress.textContent || '';
if (!address || address === '-') return;
copyText(address);
}
function copyOrderID() {
var orderID =
state.order && state.order.order_id
? state.order.order_id
: '';
if (!orderID) return;
copyText(orderID);
}
function copyText(text) {
var copied = window.navigator.clipboard
? window.navigator.clipboard.writeText(text)
: fallbackCopy(text);
Promise.resolve(copied)
.then(function () {
toast(t('recharge.copied', 'Copied'));
})
.catch(function () {
toast(t('recharge.copyFailed', 'Copy failed'));
});
}
function fallbackCopy(text) {
var input = document.createElement('textarea');
input.value = text;
input.style.position = 'fixed';
input.style.opacity = '0';
document.body.appendChild(input);
input.select();
document.execCommand('copy');
document.body.removeChild(input);
}
// 选择对象每次从 options 当前快照查找,避免保存旧对象导致切账号后仍引用旧商品。
function selectedProduct() {
return (
((state.options && state.options.products) || []).find(
function (item) {
return (
productID(item) === state.selectedProductID
);
}
) || null
);
}
function selectedMethod() {
return (
availableMethods().find(function (item) {
return item.key === state.selectedMethodKey;
}) || null
);
}
// 首屏拿到 options 后必须形成一组可下单快照:默认套餐优先取后端排序第一项,支付方式优先 USDT。
// 切账号、切套餐或后端配置变更时都重新校正,避免“有 USDT 但支付区被隐藏”或旧 method_id 串到新商品。
function normalizeSelections() {
var products =
(state.options && state.options.products) || [];
if (!products.length) {
state.selectedProductID = 0;
state.selectedMethodKey = '';
return;
}
var hasSelectedProduct = products.some(function (product) {
return productID(product) === state.selectedProductID;
});
if (!hasSelectedProduct) {
state.selectedProductID = productID(products[0]);
}
var methods = availableMethods();
if (!methods.length) {
state.selectedMethodKey = '';
return;
}
var hasSelectedMethod = methods.some(function (method) {
return method.key === state.selectedMethodKey;
});
if (hasSelectedMethod) return;
var usdtMethod = methods.find(function (method) {
return method.key === 'usdt_trc20';
});
state.selectedMethodKey = (usdtMethod || methods[0]).key;
}
// token 模式不回传 display_user_id防止前端把 token 用户和输入账号混用。
function targetDisplayUserID() {
if (
state.context &&
state.context.auth_mode !== 'token' &&
state.context.account
) {
return accountPublicID(state.context.account);
}
return '';
}
function currentAccountDisplayUserID() {
return state.context && state.context.account
? accountPublicID(state.context.account)
: '';
}
function currentAccountUserID() {
if (!state.context || !state.context.account) return '';
return (
state.context.account.user_id ||
state.context.account.userId ||
state.context.account.id ||
''
);
}
function isTrueFlag(value) {
return (
value === true ||
value === 1 ||
String(value || '').toLowerCase() === 'true'
);
}
function isFreightAccount(account) {
account = account || {};
var audienceType = String(
account.audience_type || account.audienceType || ''
).toLowerCase();
return (
audienceType === 'coin_seller' ||
audienceType === 'super_freight_agent' ||
isTrueFlag(account.freight_agent) ||
isTrueFlag(account.freightAgent) ||
isTrueFlag(account.super_freight_agent) ||
isTrueFlag(account.superFreightAgent)
);
}
function currentAccountCommodityType() {
if (!state.context) return '';
var account = state.context.account || {};
var explicitType =
state.context.commodity_type ||
state.context.commodityType ||
account.commodity_type ||
account.commodityType ||
'';
if (explicitType) return explicitType;
return isFreightAccount(account) ? 'FREIGHT_GOLD' : '';
}
function orderStatusUserID() {
return currentAccountUserID() || targetDisplayUserID();
}
function isTerminalOrder(order) {
return (
!!order &&
(order.status === 'credited' ||
order.status === 'failed')
);
}
function isRedirectPaymentProvider(providerCode) {
var normalized = String(providerCode || '').toLowerCase();
return !!normalized && normalized !== 'usdt_trc20';
}
function isRedirectPaymentOrder(order) {
return (
!!order &&
isRedirectPaymentProvider(order.provider_code) &&
!!order.pay_url &&
!isTerminalOrder(order)
);
}
function externalPayReturnURL() {
try {
var url = new URL(window.location.href);
var displayUserID =
targetDisplayUserID() ||
String(nodes.displayUserID.value || '').trim();
if (displayUserID) {
url.searchParams.set(
'display_user_id',
displayUserID
);
}
if (window.HyAppAPI && window.HyAppAPI.getAppCode) {
url.searchParams.set(
'app',
window.HyAppAPI.getAppCode()
);
url.searchParams.set(
'app_code',
window.HyAppAPI.getAppCode()
);
}
url.searchParams.set(
'language',
currentLanguageForReturnURL()
);
url.searchParams.delete(RETURN_MIFAPAY_ORDER_QUERY_KEY);
return url.toString();
} catch (error) {
return window.location.href;
}
}
function currentLanguageForReturnURL() {
var raw = currentRawParams();
var keys = [
'language',
'lang',
'locale',
'app_lang',
'appLanguage',
];
for (var i = 0; i < keys.length; i += 1) {
var value = raw.get(keys[i]);
if (value) return value;
}
return document.documentElement.lang || 'en';
}
function currentRawParams() {
if (
window.HyAppParams &&
window.HyAppParams.current &&
window.HyAppParams.current.raw
) {
return window.HyAppParams.current.raw;
}
var params = new URLSearchParams(window.location.search);
var hash = window.location.hash || '';
var hashQueryIndex = hash.indexOf('?');
if (hashQueryIndex >= 0) {
var hashParams = new URLSearchParams(
hash.slice(hashQueryIndex + 1)
);
hashParams.forEach(function (value, key) {
if (!params.has(key)) {
params.set(key, value);
}
});
}
return params;
}
function selectPayCountryFromConfig(context) {
var configured = selectedConfiguredPayCountry(context);
if (configured) {
var configuredCode =
primaryConfiguredCountryCode(configured);
var resolved =
payCountryFromContext(
context,
userCountryCodeFromContext(context)
) ||
payCountryFromContext(context, configuredCode) ||
{};
// app JSON 只控制国家和支付方式是否展示;后台 payCountryId 必须从账号 context 的国家映射里取,
// 否则 Yumi 这类旧下单接口会在商品已展示后因为 payCountryId 为空被参数校验拦截。
state.selectedPayCountryID =
configured.pay_country_id ||
configured.payCountryId ||
configured.id ||
resolved.pay_country_id ||
resolved.payCountryId ||
resolved.id ||
'';
state.selectedCountryCode = String(
configuredCode ||
resolved.country_code ||
resolved.countryCode ||
''
).toUpperCase();
state.selectedCurrencyCode =
configured.currency_code ||
configured.currencyCode ||
resolved.currency_code ||
resolved.currencyCode ||
state.selectedCurrencyCode ||
'';
return configured;
}
if (configuredMifaPayCountries().length) {
// Aslan H5 只按接口返回的用户国家匹配本地支付 JSON没有对应国家时不兜底到其它国家。
state.selectedPayCountryID = '';
state.selectedCountryCode =
userCountryCodeFromContext(context);
state.selectedCurrencyCode = '';
return null;
}
var countries =
(context &&
(context.country_list || context.countryList)) ||
[];
var selected = countries[0] || {};
state.selectedPayCountryID =
selected.pay_country_id ||
selected.payCountryId ||
selected.id ||
'';
state.selectedCountryCode = String(
selected.country_code ||
selected.countryCode ||
firstValue(selected.country || {}, [
'alphaTwo',
'alpha_two',
'countryCode',
'country_code',
]) ||
''
).toUpperCase();
state.selectedCurrencyCode =
selected.currency_code ||
selected.currencyCode ||
state.selectedCurrencyCode ||
'';
return selected && Object.keys(selected).length
? selected
: null;
}
function selectedConfiguredPayCountry(context) {
var countries = configuredMifaPayCountries();
if (!countries.length) return null;
var userCountryCode = userCountryCodeFromContext(context);
if (!userCountryCode) return null;
return (
countries.find(function (country) {
return configuredCountryMatchesCode(
country,
userCountryCode
);
}) || null
);
}
function configuredMifaPayCountries() {
var config = state.appConfig || {};
var providers = config.providers || {};
var mifapay = providers.mifapay || {};
return Array.isArray(mifapay.countries)
? mifapay.countries
: [];
}
function primaryConfiguredCountryCode(country) {
return configuredCountryCodes(country)[0] || '';
}
function configuredCountryMatchesCode(country, code) {
code = String(code || '')
.trim()
.toUpperCase();
if (!code) return false;
return configuredCountryCodes(country).indexOf(code) >= 0;
}
function configuredCountryCodes(country) {
country = country || {};
var rawCodes = []
.concat(country.country_code || [])
.concat(country.countryCode || [])
.concat(country.country_codes || [])
.concat(country.countryCodes || []);
var codes = [];
rawCodes.forEach(function (value) {
if (Array.isArray(value)) {
value.forEach(function (item) {
pushConfiguredCountryCode(codes, item);
});
return;
}
pushConfiguredCountryCode(codes, value);
});
return codes;
}
function pushConfiguredCountryCode(codes, value) {
value = String(value || '')
.trim()
.toUpperCase();
if (value && codes.indexOf(value) < 0) {
codes.push(value);
}
}
function payCountryFromContext(context, countryCode) {
var countries =
(context &&
(context.country_list || context.countryList)) ||
[];
var normalizedCountryCode = String(countryCode || '')
.trim()
.toUpperCase();
if (!normalizedCountryCode || !countries.length) {
return {};
}
return (
countries.find(function (item) {
return (
String(
item.country_code ||
item.countryCode ||
firstValue(item.country || {}, [
'alphaTwo',
'alpha_two',
'countryCode',
'country_code',
]) ||
''
).toUpperCase() === normalizedCountryCode
);
}) || null
);
}
function userCountryCodeFromContext(context) {
context = context || {};
var account = (context && context.account) || {};
return String(
account.country_code ||
account.countryCode ||
context.country_code ||
context.countryCode ||
''
).toUpperCase();
}
function emptyOptionsForSelectedCountry(context) {
return {
products: [],
payment_methods: [],
pay_country_id: state.selectedPayCountryID,
country_code:
state.selectedCountryCode ||
userCountryCodeFromContext(context),
currency_code: state.selectedCurrencyCode,
usd_to_currency_rate: 0,
usdt_trc20_enabled: false,
};
}
function applyResolvedPayCountry(options) {
options = options || {};
state.selectedPayCountryID =
options.pay_country_id ||
options.payCountryId ||
state.selectedPayCountryID ||
'';
state.selectedCountryCode = String(
options.country_code ||
options.countryCode ||
state.selectedCountryCode ||
''
).toUpperCase();
state.selectedCurrencyCode =
options.currency_code ||
options.currencyCode ||
state.selectedCurrencyCode ||
'';
}
// command_id 给服务端幂等使用,包含时间、商品、方式和随机后缀;真实订单号仍由后端生成。
// 三方拒单会把本地订单置失败,后续重用同一 command_id 只会回放旧失败结果,所以每次点击都必须生成新命令。
function commandID(product, method) {
return [
'h5',
Date.now(),
productID(product),
method.key.replace(/[^a-zA-Z0-9_:-]/g, ''),
randomCommandSuffix(),
].join(':');
}
function productID(product) {
product = product || {};
return String(
product.product_id ||
product.productId ||
product.id ||
''
);
}
function createOrderProductID(product) {
var rawProductID = productID(product);
if (isLegacyWebPayApp()) return rawProductID;
// 默认 Lalu 走 Go gatewayproduct_id 必须是 JSON number非法值转 0让后端返回参数错误而不是 JSON 解码错误。
var numericProductID = Number(rawProductID);
return isFinite(numericProductID) ? numericProductID : 0;
}
function isLegacyWebPayApp() {
var adapter =
state.appConfig &&
String(state.appConfig.adapter || '');
// Aslan/Yumi 仍走旧支付网关,保持原来的字符串商品 ID避免影响老接口的字段兼容。
return (
adapter === 'aslan-web-pay' ||
adapter === 'yumi-web-pay'
);
}
function randomCommandSuffix() {
if (
window.crypto &&
typeof window.crypto.randomUUID === 'function'
) {
return window.crypto
.randomUUID()
.replace(/-/g, '')
.slice(0, 16);
}
return Math.random().toString(36).slice(2, 14);
}
// Yumi 商品接口会直接返回本币 amount优先用它避免把 1 USD 误显示成 1 EGP。
// 其他应用没有本币商品金额时,继续按后台 USD->本币汇率展示。
function convertedAmount(product, method) {
var currency = method.currency_code || 'USD';
var localAmount = productLocalAmount(product, currency);
if (localAmount > 0) {
return (
localAmount.toFixed(
currencyFractionDigits(currency)
) +
' ' +
currency
);
}
var usd = productUSD(product);
var rate = Number(method.usd_to_currency_rate || 0);
var amount =
Number.isFinite(rate) && rate > 0 ? usd * rate : usd;
return (
amount.toFixed(currencyFractionDigits(currency)) +
' ' +
currency
);
}
function productLocalAmount(product, currency) {
var productCurrency = String(
product.currency_code || product.currencyCode || ''
).toUpperCase();
var methodCurrency = String(currency || '').toUpperCase();
if (
productCurrency &&
methodCurrency &&
productCurrency !== methodCurrency
) {
return 0;
}
var amount = Number(
product.local_amount ||
product.localAmount ||
product.amount ||
product.amountLocal ||
product.amount_local ||
0
);
return Number.isFinite(amount) && amount > 0 ? amount : 0;
}
function productUSD(product) {
var directUSDT = Number(
product.amount_usdt || product.amountUsdt || 0
);
if (Number.isFinite(directUSDT) && directUSDT > 0) {
return directUSDT;
}
var usdtMicro = Number(
product.amount_usdt_micro ||
product.amountUsdtMicro ||
product.amount_micro ||
product.amountMicro ||
0
);
if (Number.isFinite(usdtMicro) && usdtMicro > 0) {
return usdtMicro / 1000000;
}
var minor = Number(
product.amount_minor || product.amountMinor || 0
);
if (Number.isFinite(minor) && minor > 0) {
return minor / 100;
}
return 0;
}
function formatUSD(product) {
return productUSD(product).toFixed(2) + ' USD';
}
function formatCoins(value) {
var numberValue = Number(value || 0);
return Number.isFinite(numberValue)
? numberValue.toLocaleString()
: '0';
}
function statusLabel(status) {
var key = 'recharge.status.' + String(status || 'pending');
return t(key, status || 'pending');
}
// 弹窗摘要优先使用后端订单快照,恢复三方支付订单时也能脱离当前选中商品显示正确金额。
function orderAmountText(order) {
if (!order) return '-';
if (order.provider_code === 'usdt_trc20') {
return amountMinorText(order.usd_minor_amount, 'USD');
}
return amountMinorText(
order.provider_amount_minor,
order.currency_code || 'USD'
);
}
function amountMinorText(value, currency) {
var minor = Number(value || 0);
if (!Number.isFinite(minor) || minor <= 0) return '-';
return (
(minor / 100).toFixed(
currencyFractionDigits(currency)
) +
' ' +
currency
);
}
function currencyFractionDigits(currency) {
var normalized = String(currency || '').toUpperCase();
var zeroDecimal = {
BIF: true,
CLP: true,
DJF: true,
GNF: true,
IDR: true,
JPY: true,
KMF: true,
KRW: true,
MGA: true,
PYG: true,
RWF: true,
UGX: true,
VND: true,
VUV: true,
XAF: true,
XOF: true,
XPF: true,
};
return zeroDecimal[normalized] ? 0 : 2;
}
function shortOrderID(value) {
value = String(value || '');
if (!value) return '-';
return value.length > 18
? value.slice(0, 8) + '...' + value.slice(-4)
: value;
}
// loading 同时锁住确认账号和创建订单,避免用户在请求中切账号或重复下单。
function setBusy(loading) {
state.loading = loading;
nodes.searchAccount.disabled = loading;
nodes.createOrder.disabled =
loading ||
!state.selectedProductID ||
!state.selectedMethodKey;
}
function setStatus(node, message) {
node.textContent = message || '';
}
function firstLetter(value) {
return (
String(value || 'H')
.trim()
.charAt(0) || 'H'
);
}
function accountMetaText(account) {
return [
accountPublicID(account),
accountCountryText(account),
]
.filter(Boolean)
.join(' · ');
}
function accountPublicID(account) {
account = account || {};
// Aslan 的 `account` 是用户输入和外部可识别的短号,`user_id/display_user_id` 是订单入账用的内部长 ID。
// 页面展示和回跳参数优先使用短号,避免客服搜索短号后又看到不可读的内部 ID。
return (
account.account ||
account.short_id ||
account.shortId ||
account.display_user_id ||
account.displayUserId ||
account.user_id ||
account.userId ||
''
);
}
// 后端账号字段可能返回中文展示名;非中文页面优先按 code/英文字段展示,避免 EN 页面混入中文地区。
function accountCountryText(account) {
var code = firstValue(account, [
'country_code',
'countryCode',
]);
var localized = countryNameFromCode(code);
if (localized) return localized;
return displayTextOrCode(
firstValue(account, [
'country_name_en',
'countryNameEn',
'country_english_name',
'countryEnglishName',
'country_display_name_en',
'countryDisplayNameEn',
'country_name',
'countryName',
'country_display_name',
'countryDisplayName',
'country',
]),
code
);
}
function countryNameFromCode(code) {
code = String(code || '')
.trim()
.toUpperCase();
if (!/^[A-Z]{2}$/.test(code)) return '';
try {
var displayNames = new Intl.DisplayNames(
[currentDisplayLang()],
{ type: 'region' }
);
return displayNames.of(code) || code;
} catch (error) {
return code;
}
}
function displayTextOrCode(value, code) {
value = String(value || '').trim();
if (!value) return '';
if (currentDisplayLang() === 'zh') return value;
var mapped = chineseLocationNameMap()[value];
if (mapped) return mapped;
if (containsChinese(value)) return String(code || '');
return value;
}
function containsChinese(value) {
return /[\u3400-\u9fff]/.test(String(value || ''));
}
function currentDisplayLang() {
return window.HyAppI18n && window.HyAppI18n.lang
? window.HyAppI18n.lang()
: 'en';
}
function firstValue(source, keys) {
source = source || {};
for (var i = 0; i < keys.length; i += 1) {
var value = source[keys[i]];
if (
value !== undefined &&
value !== null &&
value !== ''
) {
return value;
}
}
return '';
}
function chineseLocationNameMap() {
return {
新加坡: 'Singapore',
};
}
function toast(message) {
if (window.HyAppToast) window.HyAppToast.show(message);
}
function t(key, fallback) {
return window.HyAppI18n
? window.HyAppI18n.t(key, fallback)
: fallback || key;
}
function escapeHTML(value) {
return String(value || '').replace(
/[&<>"']/g,
function (char) {
return {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
}[char];
}
);
}
function escapeAttr(value) {
return escapeHTML(value).replace(/`/g, '&#96;');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>
</body>
</html>