mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
feat: add subscription reissue button with cooldown timer
- Add revokeSubscription API method - Add reissue button in Additional Options with amber styling, destructive confirm dialog, 15-min cooldown with countdown timer, localStorage persistence across page reloads - Add i18n keys in all 4 locales (en, ru, zh, fa)
This commit is contained in:
@@ -202,6 +202,9 @@ export default function Subscription() {
|
||||
|
||||
// Traffic refresh state
|
||||
const [trafficRefreshCooldown, setTrafficRefreshCooldown] = useState(0);
|
||||
|
||||
// Revoke (reissue) cooldown state
|
||||
const [revokeCooldown, setRevokeCooldown] = useState(0);
|
||||
const [trafficData, setTrafficData] = useState<{
|
||||
traffic_used_gb: number;
|
||||
traffic_used_percent: number;
|
||||
@@ -464,6 +467,41 @@ export default function Subscription() {
|
||||
return () => clearInterval(timer);
|
||||
}, [trafficRefreshCooldown]);
|
||||
|
||||
// Initialize revoke cooldown from localStorage on mount
|
||||
useEffect(() => {
|
||||
const ts = localStorage.getItem(`revoke_ts_${subscriptionId ?? 'default'}`);
|
||||
if (ts) {
|
||||
const elapsed = Math.floor((Date.now() - parseInt(ts, 10)) / 1000);
|
||||
const remaining = Math.max(0, 900 - elapsed);
|
||||
setRevokeCooldown(remaining);
|
||||
}
|
||||
}, [subscriptionId]);
|
||||
|
||||
// Countdown timer for revoke cooldown
|
||||
useEffect(() => {
|
||||
if (revokeCooldown <= 0) return;
|
||||
const timer = setInterval(() => {
|
||||
setRevokeCooldown((prev) => Math.max(0, prev - 1));
|
||||
}, 1000);
|
||||
return () => clearInterval(timer);
|
||||
}, [revokeCooldown]);
|
||||
|
||||
// Revoke (reissue) subscription mutation
|
||||
const revokeMutation = useMutation({
|
||||
mutationFn: () => subscriptionApi.revokeSubscription(subscriptionId),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['subscription'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['connection-link', subscriptionId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] });
|
||||
haptic.notification('success');
|
||||
localStorage.setItem(`revoke_ts_${subscriptionId ?? 'default'}`, Date.now().toString());
|
||||
setRevokeCooldown(900);
|
||||
},
|
||||
onError: () => {
|
||||
haptic.notification('error');
|
||||
},
|
||||
});
|
||||
|
||||
// Auto-refresh traffic on mount (with 30s caching)
|
||||
useEffect(() => {
|
||||
if (!subscription) return;
|
||||
@@ -494,6 +532,16 @@ export default function Subscription() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRevoke = async () => {
|
||||
const confirmed = await destructiveConfirm(
|
||||
t('subscription.revoke.warning'),
|
||||
t('subscription.revoke.confirmBtn'),
|
||||
t('subscription.revoke.title'),
|
||||
);
|
||||
if (!confirmed) return;
|
||||
revokeMutation.mutate();
|
||||
};
|
||||
|
||||
// In multi-tariff mode without a specific subscription ID, redirect to list
|
||||
if (isMultiTariff && !subscriptionId && !isLoading) {
|
||||
return <Navigate to="/subscriptions" replace />;
|
||||
@@ -2244,6 +2292,53 @@ export default function Subscription() {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Reissue Subscription */}
|
||||
<div className="mt-4">
|
||||
<button
|
||||
onClick={handleRevoke}
|
||||
disabled={revokeMutation.isPending || revokeCooldown > 0}
|
||||
className="w-full rounded-xl border border-amber-500/30 bg-amber-500/10 p-4 text-left transition-colors hover:bg-amber-500/20 disabled:opacity-50"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="font-medium text-amber-400">
|
||||
{t('subscription.revoke.button')}
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-dark-400">
|
||||
{revokeCooldown > 0
|
||||
? t('subscription.revoke.cooldown', {
|
||||
minutes: Math.floor(revokeCooldown / 60),
|
||||
seconds: revokeCooldown % 60,
|
||||
})
|
||||
: t('subscription.revoke.description')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-amber-400">
|
||||
{revokeMutation.isPending ? (
|
||||
<div className="h-5 w-5 animate-spin rounded-full border-2 border-amber-400/30 border-t-amber-400" />
|
||||
) : (
|
||||
<svg
|
||||
className="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{revokeMutation.error && (
|
||||
<p className="mt-2 text-sm text-red-400">{getErrorMessage(revokeMutation.error)}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user