-
- {t('dashboard.expired.renew')}
-
+ {/* Quick Renew or Top Up button */}
+ {hasBalance ? (
+
+ ) : (
+
+ )}
+
+ {/* Renew (go to purchase page) */}
+
+
+
+
+ );
+}
diff --git a/src/components/ui/AnimatedCrossmark.tsx b/src/components/ui/AnimatedCrossmark.tsx
new file mode 100644
index 0000000..895c7be
--- /dev/null
+++ b/src/components/ui/AnimatedCrossmark.tsx
@@ -0,0 +1,37 @@
+import { motion } from 'framer-motion';
+import { cn } from '@/lib/utils';
+
+export function AnimatedCrossmark({ className }: { className?: string }) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/ui/Spinner.tsx b/src/components/ui/Spinner.tsx
new file mode 100644
index 0000000..607c9a1
--- /dev/null
+++ b/src/components/ui/Spinner.tsx
@@ -0,0 +1,17 @@
+import { useTranslation } from 'react-i18next';
+import { cn } from '@/lib/utils';
+
+export function Spinner({ className }: { className?: string }) {
+ const { t } = useTranslation();
+
+ return (
+
+ );
+}
diff --git a/src/locales/en.json b/src/locales/en.json
index 130143d..094646a 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -254,11 +254,20 @@
"trialSubtitle": "Trial period ended",
"paidSubtitle": "Subscription has expired",
"renew": "Renew Subscription",
+ "quickRenew": "Quick Renew",
+ "insufficientFunds": "Insufficient balance",
+ "renewError": "Renewal error, try again",
+ "topUp": "Top up balance",
+ "balance": "Balance",
"tariffs": "Tariffs",
"traffic": "Traffic",
"devices": "Devices",
"expiredDate": "Expired"
},
+ "suspended": {
+ "title": "Subscription Suspended",
+ "resume": "Resume"
+ },
"trialOffer": {
"freeTitle": "Free Trial Available",
"paidTitle": "Trial Subscription",
@@ -446,6 +455,7 @@
"pause": {
"title": "Subscription Pause",
"paused": "Paused",
+ "suspended": "Suspended (insufficient funds)",
"active": "Active",
"pauseBtn": "Pause",
"resumeBtn": "Resume",
@@ -677,6 +687,19 @@
"paymentSuccess": {
"title": "Payment Successful",
"message": "Your balance has been topped up successfully. The funds are now available."
+ },
+ "topUpResult": {
+ "awaitingPayment": "Awaiting Payment",
+ "awaitingPaymentDesc": "We are waiting for your payment confirmation. This may take a few minutes.",
+ "topUpAmount": "Top-up amount",
+ "success": "Balance Topped Up!",
+ "successDesc": "Your balance has been topped up successfully. The funds are now available.",
+ "failed": "Payment Failed",
+ "failedDesc": "Unfortunately, the payment was not completed. Please try again or choose a different payment method.",
+ "timeout": "Taking Longer Than Expected",
+ "timeoutDesc": "Payment processing is taking longer than usual. You can try checking the status again.",
+ "goToBalance": "Go to Balance",
+ "tryAgain": "Try Again"
}
},
"referral": {
diff --git a/src/locales/fa.json b/src/locales/fa.json
index 89df8b6..93f9501 100644
--- a/src/locales/fa.json
+++ b/src/locales/fa.json
@@ -219,7 +219,27 @@
"noActiveSubscription": "اشتراک فعال ندارید",
"devicesUsed": "دستگاهها: {{used}} از {{total}}",
"trafficUsed": "ترافیک: {{used}} از {{total}} گیگ",
- "unlimitedTraffic": "ترافیک نامحدود"
+ "unlimitedTraffic": "ترافیک نامحدود",
+ "expired": {
+ "title": "اشتراک منقضی شده",
+ "trialTitle": "دوره آزمایشی منقضی شده",
+ "trialSubtitle": "دوره آزمایشی پایان یافته",
+ "paidSubtitle": "اشتراک منقضی شده",
+ "renew": "تمدید اشتراک",
+ "quickRenew": "تمدید سریع",
+ "insufficientFunds": "موجودی ناکافی",
+ "renewError": "خطا در تمدید، دوباره تلاش کنید",
+ "topUp": "شارژ موجودی",
+ "balance": "موجودی",
+ "tariffs": "تعرفهها",
+ "traffic": "ترافیک",
+ "devices": "دستگاهها",
+ "expiredDate": "منقضی شده"
+ },
+ "suspended": {
+ "title": "اشتراک معلق شده",
+ "resume": "از سرگیری"
+ }
},
"subscription": {
"title": "اشتراک",
@@ -428,6 +448,7 @@
"nextCharge": "تا کسر بعدی",
"pauseBtn": "توقف",
"paused": "متوقف شده",
+ "suspended": "معلق شده (موجودی ناکافی)",
"pausedDescription": "کسر متوقف شد. اشتراک فعال خواهد بود تا",
"pausedInfo": "اشتراک متوقف شده",
"resumeBtn": "ادامه",
@@ -511,6 +532,19 @@
"paymentSuccess": {
"title": "پرداخت موفق",
"message": "موجودی شما با موفقیت شارژ شد. وجوه اکنون در دسترس است."
+ },
+ "topUpResult": {
+ "awaitingPayment": "در انتظار پرداخت",
+ "awaitingPaymentDesc": "ما منتظر تأیید پرداخت شما هستیم. این ممکن است چند دقیقه طول بکشد.",
+ "topUpAmount": "مبلغ شارژ",
+ "success": "شارژ موفق!",
+ "successDesc": "موجودی شما با موفقیت شارژ شد. وجوه اکنون در دسترس است.",
+ "failed": "پرداخت ناموفق",
+ "failedDesc": "متأسفانه پرداخت تکمیل نشد. لطفاً دوباره تلاش کنید یا روش پرداخت دیگری انتخاب کنید.",
+ "timeout": "بیشتر از حد معمول طول کشید",
+ "timeoutDesc": "پردازش پرداخت بیشتر از حد معمول طول میکشد. میتوانید دوباره وضعیت را بررسی کنید.",
+ "goToBalance": "مشاهده موجودی",
+ "tryAgain": "تلاش مجدد"
}
},
"referral": {
diff --git a/src/locales/ru.json b/src/locales/ru.json
index 4f5022e..9fc892c 100644
--- a/src/locales/ru.json
+++ b/src/locales/ru.json
@@ -266,11 +266,20 @@
"trialSubtitle": "Пробный период завершён",
"paidSubtitle": "Срок действия закончился",
"renew": "Продлить подписку",
+ "quickRenew": "Продлить",
+ "topUp": "Пополнить баланс",
+ "balance": "Баланс",
+ "insufficientFunds": "Недостаточно средств на балансе",
+ "renewError": "Ошибка продления, попробуйте ещё раз",
"tariffs": "Тарифы",
"traffic": "Трафик",
"devices": "Устройства",
"expiredDate": "Истекла"
},
+ "suspended": {
+ "title": "Подписка приостановлена",
+ "resume": "Возобновить"
+ },
"trialOffer": {
"freeTitle": "Бесплатный пробный период",
"paidTitle": "Пробная подписка",
@@ -469,6 +478,7 @@
"pause": {
"title": "Пауза подписки",
"paused": "На паузе",
+ "suspended": "Приостановлена (недостаточно средств)",
"active": "Активна",
"pauseBtn": "Приостановить",
"resumeBtn": "Возобновить",
@@ -705,6 +715,19 @@
"paymentSuccess": {
"title": "Оплата прошла успешно",
"message": "Ваш баланс успешно пополнен. Средства уже доступны."
+ },
+ "topUpResult": {
+ "awaitingPayment": "Ожидание оплаты",
+ "awaitingPaymentDesc": "Мы ожидаем подтверждение вашего платежа. Это может занять несколько минут.",
+ "topUpAmount": "Сумма пополнения",
+ "success": "Баланс пополнен!",
+ "successDesc": "Ваш баланс успешно пополнен. Средства уже доступны.",
+ "failed": "Оплата не прошла",
+ "failedDesc": "К сожалению, платёж не был завершён. Попробуйте ещё раз или выберите другой способ оплаты.",
+ "timeout": "Дольше, чем обычно",
+ "timeoutDesc": "Обработка платежа занимает больше времени. Вы можете проверить статус ещё раз.",
+ "goToBalance": "Перейти к балансу",
+ "tryAgain": "Попробовать снова"
}
},
"referral": {
diff --git a/src/locales/zh.json b/src/locales/zh.json
index 23640a2..008aabf 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -219,7 +219,27 @@
"noActiveSubscription": "无有效订阅",
"devicesUsed": "设备:{{used}} / {{total}}",
"trafficUsed": "流量:{{used}} / {{total}} GB",
- "unlimitedTraffic": "无限流量"
+ "unlimitedTraffic": "无限流量",
+ "expired": {
+ "title": "订阅已过期",
+ "trialTitle": "试用已过期",
+ "trialSubtitle": "试用期已结束",
+ "paidSubtitle": "订阅已到期",
+ "renew": "续订",
+ "quickRenew": "快速续订",
+ "insufficientFunds": "余额不足",
+ "renewError": "续订失败,请重试",
+ "topUp": "充值",
+ "balance": "余额",
+ "tariffs": "套餐",
+ "traffic": "流量",
+ "devices": "设备",
+ "expiredDate": "过期时间"
+ },
+ "suspended": {
+ "title": "订阅已暂停",
+ "resume": "恢复"
+ }
},
"subscription": {
"title": "订阅",
@@ -428,6 +448,7 @@
"nextCharge": "距下次扣费",
"pauseBtn": "暂停",
"paused": "已暂停",
+ "suspended": "已暂停(余额不足)",
"pausedDescription": "扣费已停止。订阅有效期至",
"pausedInfo": "订阅已暂停",
"resumeBtn": "恢复",
@@ -511,6 +532,19 @@
"paymentSuccess": {
"title": "支付成功",
"message": "您的余额已成功充值,资金现已可用。"
+ },
+ "topUpResult": {
+ "awaitingPayment": "等待付款",
+ "awaitingPaymentDesc": "我们正在等待您的付款确认。这可能需要几分钟。",
+ "topUpAmount": "充值金额",
+ "success": "充值成功!",
+ "successDesc": "您的余额已成功充值,资金现已可用。",
+ "failed": "付款失败",
+ "failedDesc": "很遗憾,付款未完成。请重试或选择其他付款方式。",
+ "timeout": "处理时间较长",
+ "timeoutDesc": "付款处理时间比平时长。您可以再次检查状态。",
+ "goToBalance": "查看余额",
+ "tryAgain": "重试"
}
},
"referral": {
diff --git a/src/pages/Balance.tsx b/src/pages/Balance.tsx
index f591ccc..a093431 100644
--- a/src/pages/Balance.tsx
+++ b/src/pages/Balance.tsx
@@ -8,12 +8,12 @@ import { useAuthStore } from '../store/auth';
import { balanceApi } from '../api/balance';
import { useCurrency } from '../hooks/useCurrency';
import { API } from '../config/constants';
-import { useToast } from '../components/Toast';
import type { PaginatedResponse, Transaction } from '../types';
import { Card } from '@/components/data-display/Card';
import { Button } from '@/components/primitives/Button';
import { staggerContainer, staggerItem } from '@/components/motion/transitions';
+import { isPaidStatus, isFailedStatus } from '../utils/paymentStatus';
// Icons
const ChevronDownIcon = ({ className = 'h-5 w-5' }: { className?: string }) => (
@@ -45,7 +45,6 @@ export default function Balance() {
const { formatAmount, currencySymbol } = useCurrency();
const [searchParams] = useSearchParams();
const navigate = useNavigate();
- const { showToast } = useToast();
const paymentHandledRef = useRef(false);
// Fetch balance from API
@@ -66,31 +65,19 @@ export default function Balance() {
if (paymentHandledRef.current) return;
const paymentStatus = searchParams.get('payment') || searchParams.get('status');
- const isSuccess =
- paymentStatus === 'success' ||
- paymentStatus === 'paid' ||
- paymentStatus === 'completed' ||
- searchParams.get('success') === 'true';
+
+ const normalised = paymentStatus?.toLowerCase() ?? '';
+ const isSuccess = isPaidStatus(normalised) || searchParams.get('success') === 'true';
+ const isFailed = isFailedStatus(normalised);
if (isSuccess) {
paymentHandledRef.current = true;
-
- refetchBalance();
- refreshUser();
- queryClient.invalidateQueries({ queryKey: ['transactions'] });
- queryClient.invalidateQueries({ queryKey: ['subscription'] });
- queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
-
- showToast({
- type: 'success',
- title: t('balance.paymentSuccess.title'),
- message: t('balance.paymentSuccess.message'),
- duration: 6000,
- });
-
- navigate('/balance', { replace: true });
+ navigate('/balance/top-up/result?status=success', { replace: true });
+ } else if (isFailed) {
+ paymentHandledRef.current = true;
+ navigate('/balance/top-up/result?status=failed', { replace: true });
}
- }, [searchParams, navigate, refetchBalance, refreshUser, queryClient, showToast, t]);
+ }, [searchParams, navigate]);
const [promocode, setPromocode] = useState('');
const [promocodeLoading, setPromocodeLoading] = useState(false);
diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx
index 2861117..0cc715c 100644
--- a/src/pages/Dashboard.tsx
+++ b/src/pages/Dashboard.tsx
@@ -235,8 +235,12 @@ export default function Dashboard() {