2893 lines
118 KiB
HTML
2893 lines
118 KiB
HTML
<!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/theme:token、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, '');
|
||
}
|
||
}
|
||
|
||
// 可用支付方式只在选中商品后生成,避免用户先选支付方式再切商品造成金额不一致。
|
||
// 三方本地支付方式必须是 active;USDT 必须同时有开关和收款地址,缺地址时不展示。
|
||
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 gateway,product_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 {
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": ''',
|
||
}[char];
|
||
}
|
||
);
|
||
}
|
||
|
||
function escapeAttr(value) {
|
||
return escapeHTML(value).replace(/`/g, '`');
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|