Files
Sub-Router/src/pages/SubDistributor.jsx
T

329 lines
13 KiB
React

import React, { useEffect, useMemo, useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import toast from 'react-hot-toast';
import { createSubDistributorOrder, getSubDistributorInfo } from '../api';
import { useAuth } from '../context/AuthContext';
import { useSite, useCurrency } from '../context/SiteContext';
function submitEpayForm(resData) {
const params = resData.data;
const url = resData.url;
if (!params || !url) return false;
const form = document.createElement('form');
form.action = url;
form.method = 'POST';
const isSafari = navigator.userAgent.indexOf('Safari') > -1 && navigator.userAgent.indexOf('Chrome') < 1;
if (!isSafari) {
form.target = '_blank';
}
Object.keys(params).forEach((key) => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = key;
input.value = params[key];
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
return true;
}
export default function SubDistributor() {
const { t } = useTranslation();
const { user, refreshUser, loading: authLoading } = useAuth();
const { site } = useSite();
const { symbol, fmtCNY } = useCurrency();
const location = useLocation();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [subInfo, setSubInfo] = useState(null);
const [submitting, setSubmitting] = useState(false);
const [cryptoOrder, setCryptoOrder] = useState(null);
const [form, setForm] = useState({
name: '',
slug: '',
payment_method: '',
chain: 'tron',
token: 'usdt',
});
useEffect(() => {
getSubDistributorInfo()
.then((res) => {
if (res.data.success) {
const info = res.data.data;
setSubInfo(info);
if (info.pay_methods?.length > 0) {
setForm((prev) => ({ ...prev, payment_method: info.pay_methods[0].type }));
}
}
})
.catch(() => {})
.finally(() => setLoading(false));
}, []);
const paymentMethods = subInfo?.pay_methods || [];
const currentPayMethod = useMemo(
() => paymentMethods.find((item) => item.type === form.payment_method),
[paymentMethods, form.payment_method]
);
const paymentReturned = useMemo(
() => new URLSearchParams(location.search).get('payment') === 'return',
[location.search]
);
useEffect(() => {
if (!paymentReturned || authLoading) return;
let cancelled = false;
const toastId = 'sub-dist-payment-return';
const checkPaymentResult = async () => {
if (!user) {
toast(t('subDist.paymentPending'), { id: toastId });
navigate('/sub-site', { replace: true });
return;
}
toast.loading(t('subDist.confirmingPayment'), { id: toastId });
const refreshed = await refreshUser({ skipErrorHandler: true });
if (cancelled) return;
if (refreshed?.has_distributor) {
toast.success(t('subDist.openedSuccess'), { id: toastId });
} else {
toast(t('subDist.paymentPending'), { id: toastId });
}
navigate('/sub-site', { replace: true });
};
checkPaymentResult();
return () => {
cancelled = true;
};
}, [authLoading, navigate, paymentReturned, refreshUser, t, user]);
const handleSubmit = async (e) => {
e.preventDefault();
if (!user) {
toast.error(t('subDist.loginRequired'));
return;
}
if (!form.name.trim() || !form.slug.trim()) {
toast.error(t('subDist.fillRequired'));
return;
}
if (!form.payment_method) {
toast.error(t('subDist.selectPayment'));
return;
}
setSubmitting(true);
setCryptoOrder(null);
try {
const payload = {
name: form.name.trim(),
slug: form.slug.trim().toLowerCase(),
payment_method: form.payment_method,
return_url: `${window.location.origin}/sub-site?payment=return`,
};
if (form.payment_method === 'crypto') {
payload.chain = form.chain;
payload.token = form.token;
}
const res = await createSubDistributorOrder(payload);
if (res.data.message === 'success') {
if (res.data.payment_type === 'stripe' && res.data.data?.pay_link) {
const opened = window.open(res.data.data.pay_link, '_blank');
if (opened) {
toast.success(t('subDist.paymentPageOpened'));
} else {
toast.error(t('subDist.popupBlocked'));
}
} else if (res.data.payment_type === 'crypto') {
setCryptoOrder(res.data.data);
toast.success(t('subDist.cryptoOrderCreated'));
} else {
if (submitEpayForm(res.data)) {
toast.success(t('subDist.paymentPageOpened'));
} else {
toast.error(t('subDist.paymentPageFailed'));
}
}
} else if (res.data.data) {
toast.error(typeof res.data.data === 'string' ? res.data.data : t('subDist.createFailed'));
} else {
toast.error(t('subDist.createFailed'));
}
} catch (e) {
// handled by interceptor
}
setSubmitting(false);
};
if (loading) {
return (
<div className="min-h-[60vh] flex items-center justify-center">
<div className="w-8 h-8 rounded-full border-2 border-white/20 border-t-white animate-spin" />
</div>
);
}
return (
<div className="max-w-5xl mx-auto px-6 py-12">
<div className="grid lg:grid-cols-[1.1fr_0.9fr] gap-6 items-start">
<div className="glass rounded-3xl p-8">
<div className="mb-8">
<p className="text-sm text-page-link font-medium mb-3">{t('subDist.badge')}</p>
<h1 className="text-3xl font-heading font-bold text-page mb-3">{t('subDist.title')}</h1>
<p className="text-sm text-page-secondary leading-6">
{t('subDist.subtitle', { name: site?.name || t('subDist.defaultSiteName') })}
</p>
</div>
{!subInfo?.enabled ? (
<div className="rounded-2xl border border-red-500/20 bg-red-500/5 p-5">
<p className="text-sm font-medium text-page mb-2">{t('subDist.notAvailable')}</p>
<p className="text-sm text-page-secondary">{subInfo?.disabled_reason || t('subDist.disabledFallback')}</p>
</div>
) : !user ? (
<div className="rounded-2xl border border-border p-5 space-y-4">
<p className="text-sm text-page-secondary">{t('subDist.loginHint')}</p>
<div className="flex flex-wrap gap-3">
<Link to="/login" className="btn-primary">{t('subDist.goLogin')}</Link>
<Link to="/register" className="btn-secondary">{t('subDist.goRegister')}</Link>
</div>
</div>
) : user?.has_distributor ? (
<div className="rounded-2xl border border-emerald-500/20 bg-emerald-500/5 p-5 space-y-3">
<p className="text-sm font-medium text-page">{t('subDist.alreadyOpenTitle')}</p>
<p className="text-sm text-page-secondary">
{t('subDist.alreadyOpenDesc', {
name: user.distributor_name || user.distributor_slug || t('subDist.defaultSiteName'),
})}
</p>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-5">
<div>
<label className="block text-sm font-medium text-page-label mb-2">{t('subDist.siteName')}</label>
<input
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
className="input"
placeholder={t('subDist.siteNamePlaceholder')}
required
/>
</div>
<div>
<label className="block text-sm font-medium text-page-label mb-2">{t('subDist.siteSlug')}</label>
<input
value={form.slug}
onChange={(e) => setForm({ ...form, slug: e.target.value.toLowerCase() })}
className="input"
placeholder="my-sub-site"
required
/>
<p className="text-xs text-page-muted mt-2">{t('subDist.siteSlugHelp')}</p>
</div>
<div>
<label className="block text-sm font-medium text-page-label mb-2">{t('subDist.paymentMethod')}</label>
<div className="grid sm:grid-cols-2 gap-3">
{paymentMethods.map((method) => (
<label
key={method.type}
className={`rounded-2xl border px-4 py-3 cursor-pointer transition-colors ${
form.payment_method === method.type
? 'border-page-link bg-page-link/10'
: 'border-border hover:bg-page-surface-hover'
}`}
>
<input
type="radio"
className="sr-only"
checked={form.payment_method === method.type}
onChange={() => setForm({ ...form, payment_method: method.type })}
/>
<div className="text-sm font-medium text-page">{method.name || method.type}</div>
</label>
))}
</div>
</div>
{form.payment_method === 'crypto' && (
<div className="grid sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-page-label mb-2">{t('subDist.chain')}</label>
<select value={form.chain} onChange={(e) => setForm({ ...form, chain: e.target.value })} className="input">
<option value="tron">TRON (TRC20)</option>
<option value="eth">Ethereum (ERC20)</option>
<option value="bsc">BSC (BEP20)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-page-label mb-2">{t('subDist.token')}</label>
<select value={form.token} onChange={(e) => setForm({ ...form, token: e.target.value })} className="input">
<option value="usdt">USDT</option>
<option value="usdc">USDC</option>
</select>
</div>
</div>
)}
<button type="submit" disabled={submitting} className="btn-primary w-full justify-center flex items-center gap-2">
{submitting && <span className="w-4 h-4 rounded-full border-2 border-white/30 border-t-white animate-spin" />}
{t('subDist.payAndOpen', { symbol, price: Number(subInfo?.price || 0).toFixed(2) })}
</button>
<p className="text-xs text-page-muted">
{t('subDist.currentUserHint', { user: user.display_name || user.username || 'User', method: currentPayMethod?.name || form.payment_method })}
</p>
<p className="text-xs text-page-muted">
{t('subDist.postPayHint')}
</p>
</form>
)}
{cryptoOrder && (
<div className="mt-6 rounded-2xl border border-emerald-500/20 bg-emerald-500/5 p-5 space-y-2">
<p className="text-sm font-medium text-page">{t('subDist.cryptoTitle')}</p>
<p className="text-sm text-page-secondary">{t('subDist.cryptoHint')}</p>
<div className="text-sm text-page">
<div>{t('subDist.wallet')}: <span className="font-mono break-all">{cryptoOrder.wallet}</span></div>
<div>{t('subDist.amount')}: <span className="font-mono">{cryptoOrder.amount} {cryptoOrder.token}</span></div>
<div>{t('subDist.tradeNo')}: <span className="font-mono break-all">{cryptoOrder.trade_no}</span></div>
</div>
</div>
)}
</div>
<div className="space-y-6">
<div className="glass rounded-3xl p-6">
<h2 className="text-lg font-semibold text-page mb-4">{t('subDist.priceCardTitle')}</h2>
<div className="space-y-4">
<div className="flex items-center justify-between text-sm">
<span className="text-page-secondary">{t('subDist.openPrice')}</span>
<span className="text-2xl font-bold text-page">{fmtCNY(subInfo?.price || 0)}</span>
</div>
<p className="text-sm text-page-secondary leading-6">{t('subDist.priceSummary')}</p>
</div>
</div>
<div className="glass rounded-3xl p-6">
<h2 className="text-lg font-semibold text-page mb-4">{t('subDist.ruleTitle')}</h2>
<div className="space-y-3 text-sm text-page-secondary leading-6">
<p>{t('subDist.rule1')}</p>
<p>{t('subDist.rule2')}</p>
<p>{t('subDist.rule3')}</p>
</div>
</div>
</div>
</div>
</div>
);
}