update payment ui and change api route path
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 597 B |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 952 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,395 @@
|
||||
/* =============================================
|
||||
iOS 26 Liquid Glass Design System
|
||||
============================================= */
|
||||
|
||||
/* ---------- CSS Variables ---------- */
|
||||
:root, [data-theme="light"] {
|
||||
--background: #f2f2f7;
|
||||
--foreground: #1c1c1e;
|
||||
--card: rgba(255,255,255,0.72);
|
||||
--card-foreground: #1c1c1e;
|
||||
--popover: rgba(255,255,255,0.88);
|
||||
--popover-foreground:#1c1c1e;
|
||||
--primary: #ffffff;
|
||||
--primary-foreground:#1c1c1e;
|
||||
--primary-hover: #e5e5ea;
|
||||
--secondary: rgba(120,120,128,0.12);
|
||||
--muted: rgba(120,120,128,0.08);
|
||||
--muted-foreground: #8e8e93;
|
||||
--accent: rgba(255,255,255,0.12);
|
||||
--accent-foreground: #1c1c1e;
|
||||
--border: rgba(60,60,67,0.12);
|
||||
--border-hover: rgba(60,60,67,0.22);
|
||||
--success: #34c759;
|
||||
--warning: #ff9500;
|
||||
--destructive: #ff3b30;
|
||||
|
||||
--glass-bg: rgba(255,255,255,0.65);
|
||||
--glass-border: rgba(255,255,255,0.8);
|
||||
--glass-shadow: 0 8px 40px rgba(0,0,0,.10), 0 1.5px 0 rgba(255,255,255,.9) inset;
|
||||
--card-shadow: 0 4px 24px rgba(0,0,0,.08);
|
||||
--popover-shadow: 0 12px 40px rgba(0,0,0,.14);
|
||||
|
||||
--blob-1: rgba(200,200,200,0.18);
|
||||
--blob-2: rgba(52,199,89,0.14);
|
||||
--blob-3: rgba(255,149,0,0.10);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--background: #000000;
|
||||
--foreground: #f5f5f7;
|
||||
--card: rgba(28,28,30,0.82);
|
||||
--card-foreground: #f5f5f7;
|
||||
--popover: rgba(44,44,46,0.92);
|
||||
--popover-foreground:#e5e5ea;
|
||||
--primary: #ffffff;
|
||||
--primary-foreground:#1c1c1e;
|
||||
--primary-hover: #e5e5ea;
|
||||
--secondary: rgba(120,120,128,0.18);
|
||||
--muted: rgba(120,120,128,0.12);
|
||||
--muted-foreground: #636366;
|
||||
--accent: rgba(255,255,255,0.15);
|
||||
--accent-foreground: #ffffff;
|
||||
--border: rgba(255,255,255,0.1);
|
||||
--border-hover: rgba(255,255,255,0.18);
|
||||
--success: #30d158;
|
||||
--warning: #ff9f0a;
|
||||
--destructive: #ff453a;
|
||||
|
||||
--glass-bg: rgba(28,28,30,0.75);
|
||||
--glass-border: rgba(255,255,255,0.10);
|
||||
--glass-shadow: 0 8px 40px rgba(0,0,0,.55), 0 1px 0 rgba(255,255,255,.06) inset;
|
||||
--card-shadow: 0 4px 32px rgba(0,0,0,.55);
|
||||
--popover-shadow: 0 12px 40px rgba(0,0,0,.5);
|
||||
|
||||
--blob-1: rgba(255,255,255,0.14);
|
||||
--blob-2: rgba(48,209,88,0.10);
|
||||
--blob-3: rgba(255,159,10,0.08);
|
||||
}
|
||||
|
||||
/* ---------- Ambient blobs ---------- */
|
||||
.blob {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
animation: blob-drift 18s ease-in-out infinite alternate;
|
||||
}
|
||||
.blob-1 {
|
||||
width: 420px; height: 420px;
|
||||
background: var(--blob-1);
|
||||
top: -120px; left: -100px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
.blob-2 {
|
||||
width: 340px; height: 340px;
|
||||
background: var(--blob-2);
|
||||
bottom: -80px; right: -60px;
|
||||
animation-delay: -6s;
|
||||
}
|
||||
.blob-3 {
|
||||
width: 260px; height: 260px;
|
||||
background: var(--blob-3);
|
||||
top: 45%; left: 55%;
|
||||
animation-delay: -12s;
|
||||
}
|
||||
@keyframes blob-drift {
|
||||
0% { transform: translate(0,0) scale(1); }
|
||||
50% { transform: translate(30px,-20px) scale(1.06); }
|
||||
100% { transform: translate(-20px,30px) scale(0.96); }
|
||||
}
|
||||
|
||||
/* ---------- Liquid Glass Card ---------- */
|
||||
.glass-card {
|
||||
background: var(--glass-bg);
|
||||
-webkit-backdrop-filter: blur(28px) saturate(1.6);
|
||||
backdrop-filter: blur(28px) saturate(1.6);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 22px;
|
||||
box-shadow: var(--glass-shadow);
|
||||
transition: background 0.3s, border-color 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
/* ---------- Flippable Status Card ---------- */
|
||||
.status-card {
|
||||
position: relative;
|
||||
perspective: 1600px;
|
||||
transition: height 0.38s cubic-bezier(.22,1,.36,1);
|
||||
}
|
||||
.status-card-inner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 0.82s cubic-bezier(.22,.9,.24,1);
|
||||
}
|
||||
.status-card.is-flipped .status-card-inner {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
.status-face {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
transform-style: preserve-3d;
|
||||
}
|
||||
.status-face-front {
|
||||
z-index: 2;
|
||||
}
|
||||
.status-face-back {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
.state-panel-shell {
|
||||
padding-top: 2px;
|
||||
}
|
||||
.state-screen {
|
||||
display: none;
|
||||
}
|
||||
.state-screen.is-active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* ---------- Timer ---------- */
|
||||
.timer-ring-track {
|
||||
stroke: var(--border);
|
||||
transition: stroke .3s;
|
||||
}
|
||||
.timer-ring-progress {
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: 50% 50%;
|
||||
transition: stroke-dashoffset 1s linear, stroke .5s;
|
||||
}
|
||||
.timer-icon {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
/* ---------- QR wrapper ---------- */
|
||||
.qr-wrapper {
|
||||
background: #ffffff;
|
||||
border-radius: 18px;
|
||||
padding: 14px;
|
||||
box-shadow: 0 2px 16px rgba(0,0,0,.12), 0 0 0 1px rgba(0,0,0,.04);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* ---------- iOS Pill button ---------- */
|
||||
.ios-pill {
|
||||
background: var(--glass-bg);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(1.4);
|
||||
backdrop-filter: blur(16px) saturate(1.4);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 99px;
|
||||
padding: 6px 13px;
|
||||
color: var(--card-foreground);
|
||||
transition: background 0.15s, opacity 0.15s;
|
||||
}
|
||||
.ios-pill:hover { opacity: 0.82; }
|
||||
.ios-pill:active { opacity: 0.6; }
|
||||
.ios-pill.w-9 { padding: 0; justify-content: center; }
|
||||
|
||||
/* ---------- iOS Rows ---------- */
|
||||
.ios-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14px 16px;
|
||||
transition: background 0.12s;
|
||||
}
|
||||
.ios-row:active { background: var(--muted); }
|
||||
|
||||
/* ---------- Icon button ---------- */
|
||||
.ios-icon-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px; height: 32px;
|
||||
border-radius: 10px;
|
||||
color: var(--muted-foreground);
|
||||
background: var(--secondary);
|
||||
transition: background 0.15s, color 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ios-icon-btn:hover { background: var(--accent); color: var(--primary); }
|
||||
|
||||
/* ---------- Primary CTA button ---------- */
|
||||
.ios-btn-primary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
padding: 16px 24px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.2px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, transform 0.1s, opacity 0.15s;
|
||||
box-shadow: 0 4px 18px rgba(0,0,0,0.12), 0 1px 0 rgba(255,255,255,.2) inset;
|
||||
}
|
||||
[data-theme="dark"] .ios-btn-primary {
|
||||
box-shadow: 0 4px 18px rgba(255,255,255,0.18), 0 1px 0 rgba(255,255,255,.12) inset;
|
||||
}
|
||||
.ios-btn-primary:hover { background: var(--primary-hover); }
|
||||
.ios-btn-primary:active { transform: scale(0.97); opacity: 0.9; }
|
||||
.ios-btn-primary:disabled { opacity: 0.45; cursor: not-allowed; transform: none; }
|
||||
|
||||
/* ---------- Secondary button ---------- */
|
||||
.ios-btn-secondary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--secondary);
|
||||
color: var(--card-foreground);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 13px;
|
||||
padding: 11px 28px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, opacity 0.15s;
|
||||
}
|
||||
.ios-btn-secondary:hover { background: var(--accent); }
|
||||
.ios-btn-secondary:active { opacity: 0.7; }
|
||||
|
||||
/* ---------- State icon circle ---------- */
|
||||
.ios-state-icon {
|
||||
width: 76px; height: 76px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ---------- Context Menu ---------- */
|
||||
.ios-menu {
|
||||
background: var(--popover);
|
||||
-webkit-backdrop-filter: blur(30px) saturate(1.8);
|
||||
backdrop-filter: blur(30px) saturate(1.8);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--popover-shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
.ios-menu.is-open {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
pointer-events: all;
|
||||
}
|
||||
.ios-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 11px 15px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--card-foreground);
|
||||
cursor: pointer;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.ios-menu-item:hover { background: var(--accent); }
|
||||
.ios-menu-item.is-selected { color: var(--primary); font-weight: 600; }
|
||||
.ios-menu-item + .ios-menu-item { border-top: 1px solid var(--border); }
|
||||
|
||||
/* ---------- Select states ---------- */
|
||||
.select-trigger.is-open { opacity: 0.8; }
|
||||
.select-trigger.is-open .select-chevron { transform: rotate(180deg); }
|
||||
|
||||
/* ---------- Toast ---------- */
|
||||
#toast {
|
||||
position: fixed;
|
||||
bottom: 36px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(12px);
|
||||
background: rgba(28,28,30,0.88);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
backdrop-filter: blur(20px);
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
padding: 10px 20px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255,255,255,0.12);
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,.3);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.22s, transform 0.22s;
|
||||
z-index: 9999;
|
||||
white-space: nowrap;
|
||||
}
|
||||
[data-theme="light"] #toast {
|
||||
background: rgba(44,44,46,0.9);
|
||||
}
|
||||
|
||||
/* ---------- Animations ---------- */
|
||||
@keyframes state-in {
|
||||
from { opacity: 0; transform: translateY(12px) scale(0.96); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
|
||||
/* ---------- View Transition ---------- */
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation: none;
|
||||
mix-blend-mode: normal;
|
||||
}
|
||||
::view-transition-old(root) { z-index: 1; }
|
||||
::view-transition-new(root) { z-index: 9; }
|
||||
|
||||
/* ---------- Debug toolbar ---------- */
|
||||
#debug-bar {
|
||||
position: fixed; bottom: 20px; right: 20px; z-index: 9999;
|
||||
display: flex; flex-direction: column; align-items: flex-end; gap: 8px;
|
||||
}
|
||||
#debug-toggle {
|
||||
width: 36px; height: 36px; border-radius: 50%; border: none;
|
||||
background: #7c3aed; color: #fff; cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
box-shadow: 0 2px 12px rgba(124,58,237,.5);
|
||||
transition: background .15s, transform .1s;
|
||||
}
|
||||
#debug-toggle:hover { background: #6d28d9; }
|
||||
#debug-toggle:active { transform: scale(.92); }
|
||||
#debug-panel {
|
||||
display: none; flex-direction: column; gap: 5px;
|
||||
background: rgba(15,15,20,.92);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(124,58,237,.35); border-radius: 14px;
|
||||
padding: 12px; min-width: 150px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,.5);
|
||||
}
|
||||
#debug-panel.is-open { display: flex; }
|
||||
.debug-label {
|
||||
font-size: 10px; font-weight: 700; color: #a78bfa;
|
||||
letter-spacing: .12em; text-transform: uppercase;
|
||||
margin-bottom: 4px; padding-bottom: 6px;
|
||||
border-bottom: 1px solid rgba(255,255,255,.08);
|
||||
}
|
||||
.debug-btn {
|
||||
background: rgba(255,255,255,.07); color: #d1d5db;
|
||||
border: 1px solid rgba(255,255,255,.1); border-radius: 8px;
|
||||
padding: 6px 10px; font-size: 12px; font-weight: 500;
|
||||
cursor: pointer; text-align: left; font-family: inherit;
|
||||
transition: background .12s, color .12s;
|
||||
}
|
||||
.debug-btn:hover { background: rgba(255,255,255,.14); color: #fff; }
|
||||
.debug-btn--reset:hover { border-color: #60a5fa; color: #93c5fd; }
|
||||
.debug-btn--success:hover { border-color: #34d399; color: #6ee7b7; }
|
||||
.debug-btn--expired:hover { border-color: #f87171; color: #fca5a5; }
|
||||
.debug-btn--timeout:hover { border-color: #fb923c; color: #fdba74; }
|
||||
.debug-btn--urgent:hover { border-color: #facc15; color: #fde68a; }
|
||||
.debug-btn--not-found {
|
||||
background: rgba(142,142,147,.15);
|
||||
color: #8e8e93;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.status-card,
|
||||
.status-card-inner,
|
||||
.status-face,
|
||||
.timer-ring-progress {
|
||||
transition-duration: 0.01ms !important;
|
||||
animation-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,585 @@
|
||||
|
||||
|
||||
// DOM helpers
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const $$ = (sel) => document.querySelectorAll(sel);
|
||||
|
||||
// ---- Network Tag ----
|
||||
const _CHAIN_PRESETS = {
|
||||
Ethereum: { bg: 'hsla(0,0%,55%,0.25)', color: 'hsla(0,0%,55%,1)', icon: 'ethereum' },
|
||||
Solana: { bg: 'hsla(256,85%,65%,0.25)', color: 'hsla(256,85%,65%,1)', icon: 'solana' },
|
||||
BSC: { bg: 'hsla(46,91%,49%,0.25)', color: 'hsla(46,91%,49%,1)', icon: 'bsc' },
|
||||
Arbitrum: { bg: 'hsla(202,100%,54%,0.25)', color: 'hsla(202,100%,54%,1)', icon: 'arbitrum' },
|
||||
TRON: { bg: 'hsla(350,100%,47%,0.20)', color: 'hsla(350,100%,47%,1)', icon: 'tron' },
|
||||
Polygon: { bg: 'hsla(263,73%,56%,0.25)', color: 'hsla(263,73%,56%,1)', icon: 'polygon' },
|
||||
Base: { bg: 'hsla(240,100%,61%,0.25)', color: 'hsla(240,100%,61%,1)', icon: 'base' },
|
||||
OP: { bg: 'hsla(353,99%,51%,0.25)', color: 'hsla(353,99%,51%,1)', icon: 'op' },
|
||||
HyperEVM: { bg: 'hsla(166,94%,79%,0.20)', color: 'hsla(166,94%,79%,1)', icon: 'hyperevm' },
|
||||
Plasma: { bg: 'hsla(166,64%,32%,0.25)', color: 'hsla(166,64%,32%,1)', icon: 'plasma' },
|
||||
Bitcoin: { bg: 'hsla(33,93%,54%,0.20)', color: 'hsla(33,93%,54%,1)', icon: 'bitcoin' },
|
||||
Binance: { bg: 'hsla(46,91%,49%,0.24)', color: 'hsla(46,91%,49%,1)', icon: 'binance' },
|
||||
};
|
||||
const _CHAIN_ALIASES = {
|
||||
evm:'Ethereum', eth:'Ethereum', ethereum:'Ethereum',
|
||||
bsc:'BSC', bnb:'BSC',
|
||||
arbitrum:'Arbitrum', arb:'Arbitrum',
|
||||
sol:'Solana', solana:'Solana',
|
||||
tron:'TRON', trx:'TRON',
|
||||
polygon:'Polygon', matic:'Polygon', pol:'Polygon',
|
||||
base:'Base',
|
||||
op:'OP', optimism:'OP',
|
||||
hyperevm:'HyperEVM', hyperliquid:'HyperEVM', hype:'HyperEVM',
|
||||
plasma:'Plasma', xpl:'Plasma',
|
||||
bitcoin:'Bitcoin', btc:'Bitcoin',
|
||||
binance:'Binance', bnb_chain:'Binance',
|
||||
};
|
||||
function _resolveChain(name) {
|
||||
if (!name) return null;
|
||||
const k = name.trim().toLowerCase();
|
||||
const canonical = _CHAIN_ALIASES[k] || Object.keys(_CHAIN_PRESETS).find(c => c.toLowerCase() === k);
|
||||
return canonical ? { label: canonical, ..._CHAIN_PRESETS[canonical] } : null;
|
||||
}
|
||||
function networkTag(network) {
|
||||
const chain = _resolveChain(network);
|
||||
if (!chain) return `<span style="font-weight:600;color:var(--card-foreground)">${network}</span>`;
|
||||
return `<span style="display:inline-flex;align-items:center;gap:4px;padding:2px 8px 2px 4px;border-radius:99px;background:${chain.bg};color:${chain.color};font-size:12px;font-weight:600;line-height:1.6"><img src="/static/images/${chain.icon}.png" width="14" height="14" style="width:14px;height:14px;border-radius:50%;object-fit:cover;flex-shrink:0" />${chain.label}</span>`;
|
||||
}
|
||||
|
||||
// Initialize Lucide icons
|
||||
if (typeof lucide !== 'undefined') lucide.createIcons();
|
||||
|
||||
// Theme
|
||||
let currentTheme = localStorage.getItem('theme') || 'dark';
|
||||
applyTheme(currentTheme);
|
||||
|
||||
function applyTheme(t) {
|
||||
currentTheme = t;
|
||||
document.documentElement.setAttribute('data-theme', t);
|
||||
localStorage.setItem('theme', t);
|
||||
$('icon-moon').style.display = t === 'light' ? 'block' : 'none';
|
||||
$('icon-sun').style.display = t === 'dark' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function toggleTheme(e) {
|
||||
const next = currentTheme === 'light' ? 'dark' : 'light';
|
||||
if (!document.startViewTransition) { applyTheme(next); return; }
|
||||
const x = e ? e.clientX : window.innerWidth / 2;
|
||||
const y = e ? e.clientY : window.innerHeight / 2;
|
||||
const r = Math.hypot(Math.max(x, window.innerWidth - x), Math.max(y, window.innerHeight - y));
|
||||
const transition = document.startViewTransition(() => applyTheme(next));
|
||||
transition.ready.then(() => {
|
||||
const clip = [`circle(0px at ${x}px ${y}px)`, `circle(${r}px at ${x}px ${y}px)`];
|
||||
document.documentElement.animate(
|
||||
{ clipPath: clip },
|
||||
{ duration: 420, easing: 'ease-in-out', pseudoElement: '::view-transition-new(root)' }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// i18n
|
||||
const LANGS = {
|
||||
en: {
|
||||
scan_title: 'Scan or copy address to pay',
|
||||
amount_to_pay: 'Amount to pay',
|
||||
payment_address: 'Payment address',
|
||||
i_have_transferred: 'I have transferred',
|
||||
checking_blockchain: 'Checking blockchain…',
|
||||
expires: 'Expires',
|
||||
copied: 'Copied to clipboard',
|
||||
verifying: 'Verifying…',
|
||||
payment_success: 'Payment Successful',
|
||||
redirecting: 'Redirecting…',
|
||||
payment_expired: 'Payment Expired',
|
||||
expired_sub: 'Please initiate a new payment',
|
||||
order_id: 'Order ID',
|
||||
order_amount: 'Order amount',
|
||||
network_timeout: 'Connection Timeout',
|
||||
timeout_sub: 'Unable to connect to the payment server',
|
||||
retry: 'Retry',
|
||||
back: 'Back',
|
||||
order_not_found: 'Order Not Found',
|
||||
not_found_sub: 'The order does not exist or has already expired',
|
||||
},
|
||||
zh: {
|
||||
scan_title: '扫码或复制地址付款',
|
||||
amount_to_pay: '付款金额',
|
||||
payment_address: '付款地址',
|
||||
i_have_transferred: '我已转账',
|
||||
checking_blockchain: '链上核验中…',
|
||||
expires: '到期时间',
|
||||
copied: '已复制到剪贴板',
|
||||
verifying: '核验中…',
|
||||
payment_success: '支付成功',
|
||||
redirecting: '正在跳转…',
|
||||
payment_expired: '支付已过期',
|
||||
expired_sub: '请重新发起支付',
|
||||
order_id: '订单 ID',
|
||||
order_amount: '订单金额',
|
||||
network_timeout: '连接超时',
|
||||
timeout_sub: '无法连接至支付服务器',
|
||||
retry: '重试',
|
||||
back: '返回',
|
||||
order_not_found: '订单不存在',
|
||||
not_found_sub: '待支付订单不存在或已过期',
|
||||
},
|
||||
ja: {
|
||||
scan_title: 'アドレスをスキャンまたはコピーして支払う',
|
||||
amount_to_pay: '支払い金額',
|
||||
payment_address: '支払いアドレス',
|
||||
i_have_transferred: '送金しました',
|
||||
checking_blockchain: 'ブロックチェーン確認中…',
|
||||
expires: '有効期限',
|
||||
copied: 'コピーしました',
|
||||
verifying: '確認中…',
|
||||
payment_success: '支払い完了',
|
||||
redirecting: 'リダイレクト中…',
|
||||
payment_expired: '支払い期限切れ',
|
||||
expired_sub: '新しい支払いを開始してください',
|
||||
order_id: '注文 ID',
|
||||
order_amount: '注文金額',
|
||||
network_timeout: '接続タイムアウト',
|
||||
timeout_sub: '支払いサーバーに接続できません',
|
||||
retry: '再試行',
|
||||
back: '戻る',
|
||||
order_not_found: '注文が見つかりません',
|
||||
not_found_sub: '注文が存在しないか、すでに期限切れです',
|
||||
},
|
||||
ko: {
|
||||
scan_title: '주소를 스캔하거나 복사하여 결제',
|
||||
amount_to_pay: '결제 금액',
|
||||
payment_address: '결제 주소',
|
||||
i_have_transferred: '이체 완료',
|
||||
checking_blockchain: '블록체인 확인 중…',
|
||||
expires: '만료 시간',
|
||||
copied: '복사됨',
|
||||
verifying: '확인 중…',
|
||||
payment_success: '결제 성공',
|
||||
redirecting: '리다이렉트 중…',
|
||||
payment_expired: '결제 만료',
|
||||
expired_sub: '새로운 결제를 시작하세요',
|
||||
order_id: '주문 ID',
|
||||
order_amount: '주문 금액',
|
||||
network_timeout: '연결 시간 초과',
|
||||
timeout_sub: '결제 서버에 연결할 수 없습니다',
|
||||
retry: '다시 시도',
|
||||
back: '돌아가기',
|
||||
order_not_found: '주문 없음',
|
||||
not_found_sub: '주문이 존재하지 않거나 이미 만료되었습니다',
|
||||
},
|
||||
'zh-hk': {
|
||||
scan_title: '掃碼或複製地址付款',
|
||||
amount_to_pay: '付款金額',
|
||||
payment_address: '付款地址',
|
||||
i_have_transferred: '我已轉帳',
|
||||
checking_blockchain: '鏈上核驗中…',
|
||||
expires: '到期時間',
|
||||
copied: '已複製到剪貼簿',
|
||||
verifying: '核驗中…',
|
||||
payment_success: '支付成功',
|
||||
redirecting: '正在跳轉…',
|
||||
payment_expired: '支付已過期',
|
||||
expired_sub: '請重新發起支付',
|
||||
order_id: '訂單 ID',
|
||||
order_amount: '訂單金額',
|
||||
network_timeout: '連線逾時',
|
||||
timeout_sub: '無法連線至支付伺服器',
|
||||
retry: '重試',
|
||||
back: '返回',
|
||||
order_not_found: '訂單不存在',
|
||||
not_found_sub: '待支付訂單不存在或已過期',
|
||||
},
|
||||
ru: {
|
||||
scan_title: 'Отсканируйте или скопируйте адрес для оплаты',
|
||||
amount_to_pay: 'Сумма к оплате',
|
||||
payment_address: 'Адрес для оплаты',
|
||||
i_have_transferred: 'Я перевёл',
|
||||
checking_blockchain: 'Проверка блокчейна…',
|
||||
expires: 'Истекает',
|
||||
copied: 'Скопировано',
|
||||
verifying: 'Проверка…',
|
||||
payment_success: 'Оплата прошла успешно',
|
||||
redirecting: 'Перенаправление…',
|
||||
payment_expired: 'Срок оплаты истёк',
|
||||
expired_sub: 'Пожалуйста, создайте новый платёж',
|
||||
order_id: 'Заказ ID',
|
||||
order_amount: 'Сумма заказа',
|
||||
network_timeout: 'Тайм-аут подключения',
|
||||
timeout_sub: 'Не удалось подключиться к серверу',
|
||||
retry: 'Повторить',
|
||||
back: 'Назад',
|
||||
order_not_found: 'Заказ не найден',
|
||||
not_found_sub: 'Заказ не существует или уже истёк',
|
||||
},
|
||||
};
|
||||
|
||||
const LANG_LABELS = { en: 'EN', zh: '中文', 'zh-hk': '繁體', ja: '日本語', ko: '한국어', ru: 'RU' };
|
||||
const SUPPORTED_LANGS = Object.keys(LANGS); // ['en','zh','ja','ko','zh-hk','ru']
|
||||
let currentLang = 'en';
|
||||
const t = (key) => LANGS[currentLang]?.[key] ?? key;
|
||||
|
||||
function detectLang() {
|
||||
const saved = localStorage.getItem('lang');
|
||||
if (saved && SUPPORTED_LANGS.includes(saved)) return saved;
|
||||
for (const nav of (navigator.languages || [navigator.language])) {
|
||||
const lc = nav.toLowerCase();
|
||||
if (lc.startsWith('zh-hk') || lc.startsWith('zh-tw') || lc.startsWith('zh-mo')) return 'zh-hk';
|
||||
if (lc.startsWith('zh')) return 'zh';
|
||||
if (lc.startsWith('ja')) return 'ja';
|
||||
if (lc.startsWith('ko')) return 'ko';
|
||||
if (lc.startsWith('ru')) return 'ru';
|
||||
if (lc.startsWith('en')) return 'en';
|
||||
}
|
||||
return 'en';
|
||||
}
|
||||
|
||||
function setLang(lang) {
|
||||
currentLang = lang;
|
||||
localStorage.setItem('lang', lang);
|
||||
$$('[data-i18n]').forEach(el => {
|
||||
const v = t(el.dataset.i18n);
|
||||
if (v) el.textContent = v;
|
||||
});
|
||||
$('lang-label').textContent = LANG_LABELS[lang];
|
||||
$$('#dd-lang-menu .select-option').forEach(o =>
|
||||
o.classList.toggle('is-selected', o.dataset.lang === lang));
|
||||
closeAllSelects();
|
||||
if (ORDER.tradeId) { renderOrderId(); }
|
||||
if (ORDER.amount && !ORDER.amount.startsWith('{{')) {
|
||||
renderRow('display-fiat', t('order_amount'), `${ORDER.amount} ${ORDER.currency || ''}`);
|
||||
}
|
||||
requestAnimationFrame(syncStatusCardHeight);
|
||||
}
|
||||
|
||||
// Select dropdown
|
||||
function toggleSelect(id) {
|
||||
const wrap = $(id);
|
||||
const trigger = wrap.querySelector('.select-trigger');
|
||||
const menu = wrap.querySelector('.select-menu');
|
||||
const wasOpen = menu.classList.contains('is-open');
|
||||
closeAllSelects();
|
||||
if (!wasOpen) {
|
||||
menu.classList.add('is-open');
|
||||
trigger.classList.add('is-open');
|
||||
}
|
||||
}
|
||||
|
||||
function closeAllSelects() {
|
||||
$$('.select-menu.is-open').forEach(m => {
|
||||
m.classList.remove('is-open');
|
||||
m.closest('.select-wrap').querySelector('.select-trigger').classList.remove('is-open');
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.select-wrap')) closeAllSelects();
|
||||
});
|
||||
|
||||
// Clipboard fallback (used when ClipboardJS is unavailable)
|
||||
const CHECK_ICON = '<i data-lucide="check" width="15" height="15" stroke-width="2.5" color="#22c55e"></i>';
|
||||
|
||||
function showToast() {
|
||||
const el = $('toast');
|
||||
el.textContent = t('copied');
|
||||
el.style.opacity = '1';
|
||||
el.style.transform = 'translateX(-50%) translateY(0)';
|
||||
clearTimeout(el._tid);
|
||||
el._tid = setTimeout(() => {
|
||||
el.style.opacity = '0';
|
||||
el.style.transform = 'translateX(-50%) translateY(10px)';
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function flashCheck(btnId) {
|
||||
const btn = $(btnId);
|
||||
if (!btn) return;
|
||||
const orig = btn.innerHTML;
|
||||
btn.innerHTML = CHECK_ICON;
|
||||
lucide.createIcons({ nodes: [btn] });
|
||||
setTimeout(() => { btn.innerHTML = orig; lucide.createIcons({ nodes: [btn] }); }, 1800);
|
||||
}
|
||||
|
||||
// Transfer button
|
||||
function handleTransfer() {
|
||||
const btn = $('btn-transferred');
|
||||
const span = btn.querySelector('span');
|
||||
span.textContent = t('verifying');
|
||||
btn.disabled = true;
|
||||
_pollErrors = 0;
|
||||
_pollStopped = false;
|
||||
clearTimeout(_pollTimer);
|
||||
checkOrderStatus();
|
||||
setTimeout(() => {
|
||||
span.textContent = t('i_have_transferred');
|
||||
btn.disabled = false;
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
// Order
|
||||
function truncateAddr(addr) {
|
||||
return addr.length > 10 ? `${addr.slice(0, 4)}...${addr.slice(-6)}` : addr;
|
||||
}
|
||||
|
||||
function formatAddr(addr) {
|
||||
if (!addr || addr.length <= 10) return addr;
|
||||
const pre = addr.slice(0, 4);
|
||||
const mid = addr.slice(4, -6);
|
||||
const suf = addr.slice(-6);
|
||||
return `<span style="font-weight:800;color:var(--primary)">${pre}</span><span style="color:var(--muted-foreground)">${mid}</span><span style="font-weight:800;color:var(--primary)">${suf}</span>`;
|
||||
}
|
||||
|
||||
function renderRow(id, label, value) {
|
||||
const el = $(id);
|
||||
if (el) el.innerHTML = `<td style="width:1%;white-space:nowrap;padding-right:0.75em;">${label}</td><td style="color:var(--card-foreground);font-weight:500;word-break:break-all">${value}</td>`;
|
||||
}
|
||||
|
||||
function renderOrderId() {
|
||||
if (ORDER.tradeId && !ORDER.tradeId.startsWith('{{')) {
|
||||
renderRow('display-order-id', t('order_id'), ORDER.tradeId);
|
||||
}
|
||||
}
|
||||
|
||||
function initOrder() {
|
||||
if (!ORDER.tradeId || ORDER.tradeId.startsWith('{{')) {
|
||||
showNotFound();
|
||||
return;
|
||||
}
|
||||
|
||||
$('display-amount').textContent = `${ORDER.actualAmount} ${ORDER.token}`;
|
||||
$('field-amount').textContent = `${ORDER.actualAmount} ${ORDER.token}`;
|
||||
$('field-address').innerHTML = formatAddr(ORDER.receiveAddress);
|
||||
if (ORDER.amount && !ORDER.amount.startsWith('{{')) {
|
||||
renderRow('display-fiat', t('order_amount'), `${ORDER.amount} ${ORDER.currency || ''}`);
|
||||
}
|
||||
$('display-network').innerHTML = networkTag(ORDER.network);
|
||||
renderOrderId();
|
||||
|
||||
new QRCode($('qrcode'), {
|
||||
text: ORDER.receiveAddress,
|
||||
width: 176,
|
||||
height: 176,
|
||||
colorDark: '#111111',
|
||||
colorLight: '#ffffff',
|
||||
correctLevel: QRCode.CorrectLevel.M,
|
||||
});
|
||||
|
||||
initCountdown();
|
||||
checkOrderStatus();
|
||||
initClipboard();
|
||||
}
|
||||
|
||||
// Clipboard
|
||||
let _clipboard = null;
|
||||
|
||||
function initClipboard() {
|
||||
const targets = [
|
||||
{ id: 'copy-amt-box', text: ORDER.actualAmount, iconId: 'btn-copy-amt2' },
|
||||
{ id: 'copy-addr-box', text: ORDER.receiveAddress, iconId: 'btn-copy-addr' },
|
||||
{ id: 'btn-copy-amount', text: ORDER.actualAmount, iconId: 'btn-copy-amount' },
|
||||
];
|
||||
|
||||
if (typeof ClipboardJS !== 'undefined') {
|
||||
targets.forEach(({ id, text }) => {
|
||||
const el = $(id);
|
||||
if (el) el.setAttribute('data-clipboard-text', text);
|
||||
});
|
||||
if (_clipboard) _clipboard.destroy();
|
||||
_clipboard = new ClipboardJS(targets.map(c => '#' + c.id).join(', '));
|
||||
_clipboard.on('success', e => {
|
||||
e.clearSelection();
|
||||
const tgt = targets.find(c => c.id === e.trigger.id);
|
||||
showToast();
|
||||
if (tgt) flashCheck(tgt.iconId);
|
||||
});
|
||||
}
|
||||
}
|
||||
// Countdown color interpolation (green → orange → red by ratio)
|
||||
function lerpColor(a, b, t) {
|
||||
return `rgb(${Math.round(a[0]+(b[0]-a[0])*t)},${Math.round(a[1]+(b[1]-a[1])*t)},${Math.round(a[2]+(b[2]-a[2])*t)})`;
|
||||
}
|
||||
function ratioColor(ratio) {
|
||||
if (ratio >= 0.5) return '#22c55e';
|
||||
if (ratio >= 0.2) return lerpColor([34,197,94], [249,115,22], (0.5 - ratio) / 0.3);
|
||||
return lerpColor([249,115,22], [239,68,68], (0.2 - ratio) / 0.2);
|
||||
}
|
||||
|
||||
const CIRCUMFERENCE = 2 * Math.PI * 20; // r=20 in iOS SVG ring
|
||||
let countdownInterval = null;
|
||||
let totalSeconds = 0;
|
||||
let _expiresAt = null;
|
||||
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
|
||||
function initCountdown() {
|
||||
const parseTime = (raw) => new Date(/^\d+$/.test(String(raw)) ? Number(raw) : raw);
|
||||
_expiresAt = parseTime(ORDER.expirationTime);
|
||||
const remaining = Math.max(0, Math.round((_expiresAt - Date.now()) / 1000));
|
||||
if (remaining <= 0) { showExpired(); return; }
|
||||
// 用 createdAt 和 expirationTime 推算总有效时长,否则默认 600 秒
|
||||
const created = parseTime(ORDER.createdAt);
|
||||
totalSeconds = (!isNaN(created.getTime()) && !isNaN(_expiresAt.getTime()))
|
||||
? Math.max(1, Math.round((_expiresAt - created) / 1000))
|
||||
: 600;
|
||||
$('ring').style.strokeDasharray = CIRCUMFERENCE;
|
||||
// 让初始渲染就从 remaining-1 开始,避免视觉上停顿一秒
|
||||
_expiresAt = new Date(_expiresAt.getTime() - 1000);
|
||||
tickCountdown();
|
||||
countdownInterval = setInterval(tickCountdown, 1000);
|
||||
}
|
||||
|
||||
function tickCountdown() {
|
||||
const remaining = Math.max(0, Math.round((_expiresAt - Date.now()) / 1000));
|
||||
const h = Math.floor(remaining / 3600);
|
||||
const m = Math.floor((remaining % 3600) / 60);
|
||||
const s = remaining % 60;
|
||||
|
||||
const timeStr = h > 0
|
||||
? `${pad(h)}:${pad(m)}:${pad(s)}`
|
||||
: `${pad(m)}:${pad(s)}`;
|
||||
|
||||
$('countdown').textContent = timeStr;
|
||||
const inlineEl = $('countdown-inline');
|
||||
|
||||
const color = ratioColor(totalSeconds > 0 ? remaining / totalSeconds : 0);
|
||||
if (inlineEl) { inlineEl.textContent = timeStr; inlineEl.style.color = color; }
|
||||
|
||||
$('ring').style.strokeDashoffset = CIRCUMFERENCE * (1 - (totalSeconds > 0 ? remaining / totalSeconds : 0));
|
||||
|
||||
$('countdown').style.color = color;
|
||||
$('ring').style.stroke = color;
|
||||
|
||||
if (remaining <= 0) {
|
||||
clearInterval(countdownInterval);
|
||||
showExpired();
|
||||
}
|
||||
}
|
||||
|
||||
// Polling
|
||||
let _pollTimer = null;
|
||||
let _pollStopped = false;
|
||||
let _pollErrors = 0;
|
||||
const MAX_POLL_ERRORS = 5;
|
||||
|
||||
function checkOrderStatus() {
|
||||
if (_pollStopped || !ORDER.tradeId || ORDER.tradeId.startsWith('{{')) return;
|
||||
|
||||
fetch(`/pay/check-status/${encodeURIComponent(ORDER.tradeId)}`, {
|
||||
headers: { Accept: 'application/json' },
|
||||
signal: AbortSignal.timeout(8000),
|
||||
})
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.json();
|
||||
})
|
||||
.then(({ data }) => {
|
||||
_pollErrors = 0;
|
||||
if (data?.status === 2) onPaymentSuccess();
|
||||
else if (data?.status === 3) showExpired();
|
||||
else scheduleNextPoll();
|
||||
})
|
||||
.catch(() => {
|
||||
if (++_pollErrors >= MAX_POLL_ERRORS) showTimeout();
|
||||
else scheduleNextPoll();
|
||||
});
|
||||
}
|
||||
|
||||
const scheduleNextPoll = () => {
|
||||
if (!_pollStopped) _pollTimer = setTimeout(checkOrderStatus, 5000);
|
||||
};
|
||||
|
||||
function stopPolling() {
|
||||
_pollStopped = true;
|
||||
clearTimeout(_pollTimer);
|
||||
}
|
||||
|
||||
// State panels
|
||||
const SCREENS = ['screen-success', 'screen-expired', 'screen-timeout', 'screen-not-found'];
|
||||
|
||||
function syncStatusCardHeight() {
|
||||
const card = $('status-card');
|
||||
const inner = card?.querySelector('.status-card-inner');
|
||||
if (!card || !inner) return;
|
||||
|
||||
const activeFace = card.classList.contains('is-flipped')
|
||||
? card.querySelector('.status-face-back')
|
||||
: card.querySelector('.status-face-front');
|
||||
|
||||
const nextHeight = activeFace?.scrollHeight || 0;
|
||||
if (nextHeight > 0) {
|
||||
card.style.height = `${nextHeight}px`;
|
||||
inner.style.height = `${nextHeight}px`;
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveScreen(state) {
|
||||
SCREENS.forEach(id => $(id).classList.toggle('is-active', id === `screen-${state}`));
|
||||
}
|
||||
|
||||
function flipToState(state) {
|
||||
setActiveScreen(state);
|
||||
$('status-card').classList.add('is-flipped');
|
||||
requestAnimationFrame(syncStatusCardHeight);
|
||||
}
|
||||
|
||||
function showStatePanel(state) {
|
||||
$('order-info').style.display = state === 'not-found' ? 'none' : '';
|
||||
flipToState(state);
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
if (document.referrer) {
|
||||
window.location.href = document.referrer;
|
||||
} else if (ORDER.redirectUrl && !ORDER.redirectUrl.startsWith('{{')) {
|
||||
window.location.href = ORDER.redirectUrl;
|
||||
} else {
|
||||
history.back();
|
||||
}
|
||||
}
|
||||
|
||||
function hideStatePanel() {
|
||||
$('status-card').classList.remove('is-flipped');
|
||||
$('order-info').style.display = '';
|
||||
requestAnimationFrame(syncStatusCardHeight);
|
||||
}
|
||||
|
||||
function onPaymentSuccess() {
|
||||
stopPolling();
|
||||
clearInterval(countdownInterval);
|
||||
showStatePanel('success');
|
||||
if (ORDER.redirectUrl && !ORDER.redirectUrl.startsWith('{{')) {
|
||||
setTimeout(() => { window.location.href = ORDER.redirectUrl; }, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
function showExpired() {
|
||||
stopPolling();
|
||||
clearInterval(countdownInterval);
|
||||
$('btn-transferred').disabled = true;
|
||||
showStatePanel('expired');
|
||||
}
|
||||
|
||||
function showTimeout() {
|
||||
stopPolling();
|
||||
showStatePanel('timeout');
|
||||
}
|
||||
|
||||
function showNotFound() {
|
||||
stopPolling();
|
||||
clearInterval(countdownInterval);
|
||||
$('btn-transferred').disabled = true;
|
||||
showStatePanel('not-found');
|
||||
}
|
||||
|
||||
function retryPolling() {
|
||||
_pollErrors = 0;
|
||||
_pollStopped = false;
|
||||
hideStatePanel();
|
||||
checkOrderStatus();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
setLang(detectLang());
|
||||
initOrder();
|
||||
syncStatusCardHeight();
|
||||
window.addEventListener('resize', syncStatusCardHeight);
|
||||
});
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
line-height: 1;
|
||||
background-color: #fafbfc;
|
||||
}
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||