mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
566 lines
20 KiB
TypeScript
566 lines
20 KiB
TypeScript
/**
|
||
* Global WebSocket notifications handler.
|
||
* Listens to all WebSocket events and shows appropriate toasts or modals.
|
||
*/
|
||
|
||
import { useCallback } from 'react';
|
||
import { useNavigate } from 'react-router';
|
||
import { useTranslation } from 'react-i18next';
|
||
import { useQueryClient } from '@tanstack/react-query';
|
||
import { useWebSocket, type WSMessage } from '../hooks/useWebSocket';
|
||
import { useToast } from './Toast';
|
||
import { useAuthStore } from '../store/auth';
|
||
import { useCurrency } from '../hooks/useCurrency';
|
||
import { useSuccessNotification } from '../store/successNotification';
|
||
|
||
export default function WebSocketNotifications() {
|
||
const { t } = useTranslation();
|
||
const navigate = useNavigate();
|
||
const queryClient = useQueryClient();
|
||
const { showToast } = useToast();
|
||
const refreshUser = useAuthStore((state) => state.refreshUser);
|
||
const { formatAmount, currencySymbol } = useCurrency();
|
||
const showSuccessModal = useSuccessNotification((state) => state.show);
|
||
|
||
const handleMessage = useCallback(
|
||
(message: WSMessage) => {
|
||
const { type } = message;
|
||
|
||
// Skip ticket events - they are handled by TicketNotificationBell
|
||
if (type.startsWith('ticket.')) {
|
||
return;
|
||
}
|
||
|
||
// Balance events
|
||
if (type === 'balance.topup') {
|
||
// Show prominent success modal for balance top-up
|
||
showSuccessModal({
|
||
type: 'balance_topup',
|
||
amountKopeks: message.amount_kopeks,
|
||
newBalanceKopeks: message.new_balance_kopeks,
|
||
});
|
||
// Refresh data
|
||
queryClient.invalidateQueries({ queryKey: ['balance'] });
|
||
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
|
||
queryClient.invalidateQueries({ queryKey: ['transactions'] });
|
||
refreshUser();
|
||
return;
|
||
}
|
||
|
||
if (type === 'balance.change') {
|
||
const amount = message.amount_rubles ?? (message.amount_kopeks ?? 0) / 100;
|
||
const isPositive = amount >= 0;
|
||
showToast({
|
||
type: isPositive ? 'success' : 'info',
|
||
title: t('wsNotifications.balance.changeTitle', 'Balance updated'),
|
||
message:
|
||
message.description ||
|
||
t(
|
||
'wsNotifications.balance.changeMessage',
|
||
'Your balance has changed by {{amount}} {{currency}}',
|
||
{
|
||
amount: (isPositive ? '+' : '') + formatAmount(amount),
|
||
currency: currencySymbol,
|
||
},
|
||
),
|
||
icon: <span className="text-lg">{isPositive ? '💵' : '💸'}</span>,
|
||
onClick: () => navigate('/balance'),
|
||
duration: 5000,
|
||
});
|
||
queryClient.invalidateQueries({ queryKey: ['balance'] });
|
||
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
|
||
queryClient.invalidateQueries({ queryKey: ['transactions'] });
|
||
refreshUser();
|
||
return;
|
||
}
|
||
|
||
// Subscription events
|
||
if (type === 'subscription.activated') {
|
||
// Show prominent success modal for subscription activation
|
||
showSuccessModal({
|
||
type: 'subscription_activated',
|
||
expiresAt: message.expires_at,
|
||
tariffName: message.tariff_name,
|
||
});
|
||
queryClient.invalidateQueries({
|
||
predicate: (query) =>
|
||
Array.isArray(query.queryKey) && query.queryKey[0] === 'subscription',
|
||
});
|
||
queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] });
|
||
queryClient.invalidateQueries({ queryKey: ['balance'] });
|
||
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
|
||
refreshUser();
|
||
return;
|
||
}
|
||
|
||
if (type === 'subscription.renewed') {
|
||
// Show prominent success modal for subscription renewal
|
||
showSuccessModal({
|
||
type: 'subscription_renewed',
|
||
amountKopeks: message.amount_kopeks,
|
||
expiresAt: message.new_expires_at,
|
||
});
|
||
queryClient.invalidateQueries({
|
||
predicate: (query) =>
|
||
Array.isArray(query.queryKey) && query.queryKey[0] === 'subscription',
|
||
});
|
||
queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] });
|
||
queryClient.invalidateQueries({ queryKey: ['balance'] });
|
||
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
|
||
refreshUser();
|
||
return;
|
||
}
|
||
|
||
if (type === 'subscription.expiring') {
|
||
showToast({
|
||
type: 'warning',
|
||
title: t('wsNotifications.subscription.expiringTitle', 'Subscription expiring soon'),
|
||
message: t(
|
||
'wsNotifications.subscription.expiringMessage',
|
||
'Your subscription expires in {{days}} days',
|
||
{
|
||
days: message.days_left ?? 0,
|
||
},
|
||
),
|
||
icon: <span className="text-lg">⏰</span>,
|
||
onClick: () => navigate('/subscriptions'),
|
||
duration: 10000,
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (type === 'subscription.expired') {
|
||
showToast({
|
||
type: 'error',
|
||
title: t('wsNotifications.subscription.expiredTitle', 'Subscription expired'),
|
||
message: t(
|
||
'wsNotifications.subscription.expiredMessage',
|
||
'Your subscription has expired. Renew to continue using the service.',
|
||
),
|
||
icon: <span className="text-lg">😢</span>,
|
||
onClick: () => navigate('/subscriptions'),
|
||
duration: 10000,
|
||
});
|
||
queryClient.invalidateQueries({
|
||
predicate: (query) =>
|
||
Array.isArray(query.queryKey) && query.queryKey[0] === 'subscription',
|
||
});
|
||
queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] });
|
||
refreshUser();
|
||
return;
|
||
}
|
||
|
||
if (type === 'subscription.daily_debit') {
|
||
const amount = message.amount_rubles ?? (message.amount_kopeks ?? 0) / 100;
|
||
showToast({
|
||
type: 'info',
|
||
title: t('wsNotifications.subscription.dailyDebitTitle', 'Daily charge'),
|
||
message: t(
|
||
'wsNotifications.subscription.dailyDebitMessage',
|
||
'Daily subscription fee: {{amount}} {{currency}}',
|
||
{
|
||
amount: formatAmount(amount),
|
||
currency: currencySymbol,
|
||
},
|
||
),
|
||
icon: <span className="text-lg">📅</span>,
|
||
duration: 5000,
|
||
});
|
||
queryClient.invalidateQueries({ queryKey: ['balance'] });
|
||
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
|
||
refreshUser();
|
||
return;
|
||
}
|
||
|
||
if (type === 'subscription.traffic_reset') {
|
||
showToast({
|
||
type: 'info',
|
||
title: t('wsNotifications.subscription.trafficResetTitle', 'Traffic reset'),
|
||
message: t(
|
||
'wsNotifications.subscription.trafficResetMessage',
|
||
'Your traffic limit has been reset',
|
||
),
|
||
icon: <span className="text-lg">🔄</span>,
|
||
duration: 5000,
|
||
});
|
||
queryClient.invalidateQueries({
|
||
predicate: (query) =>
|
||
Array.isArray(query.queryKey) && query.queryKey[0] === 'subscription',
|
||
});
|
||
queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] });
|
||
return;
|
||
}
|
||
|
||
if (type === 'subscription.devices_purchased') {
|
||
// Show prominent success modal for device purchase
|
||
showSuccessModal({
|
||
type: 'devices_purchased',
|
||
amountKopeks: message.amount_kopeks,
|
||
devicesAdded: message.devices_added,
|
||
newDeviceLimit: message.new_device_limit,
|
||
});
|
||
queryClient.invalidateQueries({
|
||
predicate: (query) =>
|
||
Array.isArray(query.queryKey) && query.queryKey[0] === 'subscription',
|
||
});
|
||
queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] });
|
||
queryClient.invalidateQueries({ queryKey: ['balance'] });
|
||
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
|
||
queryClient.invalidateQueries({ queryKey: ['transactions'] });
|
||
refreshUser();
|
||
return;
|
||
}
|
||
|
||
if (type === 'subscription.traffic_purchased') {
|
||
// Show prominent success modal for traffic purchase
|
||
showSuccessModal({
|
||
type: 'traffic_purchased',
|
||
amountKopeks: message.amount_kopeks,
|
||
trafficGbAdded: message.traffic_gb_added,
|
||
newTrafficLimitGb: message.new_traffic_limit_gb,
|
||
});
|
||
queryClient.invalidateQueries({
|
||
predicate: (query) =>
|
||
Array.isArray(query.queryKey) && query.queryKey[0] === 'subscription',
|
||
});
|
||
queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] });
|
||
queryClient.invalidateQueries({ queryKey: ['balance'] });
|
||
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
|
||
queryClient.invalidateQueries({ queryKey: ['transactions'] });
|
||
refreshUser();
|
||
return;
|
||
}
|
||
|
||
// Autopay events
|
||
if (type === 'autopay.success') {
|
||
const amount = message.amount_rubles ?? (message.amount_kopeks ?? 0) / 100;
|
||
showToast({
|
||
type: 'success',
|
||
title: t('wsNotifications.autopay.successTitle', 'Auto-renewal successful'),
|
||
message: t(
|
||
'wsNotifications.autopay.successMessage',
|
||
'Your subscription was auto-renewed for {{amount}} {{currency}}',
|
||
{
|
||
amount: formatAmount(amount),
|
||
currency: currencySymbol,
|
||
},
|
||
),
|
||
icon: <span className="text-lg">🔁</span>,
|
||
onClick: () => navigate('/subscriptions'),
|
||
duration: 8000,
|
||
});
|
||
queryClient.invalidateQueries({
|
||
predicate: (query) =>
|
||
Array.isArray(query.queryKey) && query.queryKey[0] === 'subscription',
|
||
});
|
||
queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] });
|
||
queryClient.invalidateQueries({ queryKey: ['balance'] });
|
||
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
|
||
refreshUser();
|
||
return;
|
||
}
|
||
|
||
if (type === 'autopay.failed') {
|
||
showToast({
|
||
type: 'error',
|
||
title: t('wsNotifications.autopay.failedTitle', 'Auto-renewal failed'),
|
||
message:
|
||
message.reason ||
|
||
t('wsNotifications.autopay.failedMessage', 'Failed to auto-renew your subscription'),
|
||
icon: <span className="text-lg">❌</span>,
|
||
onClick: () => navigate('/subscriptions'),
|
||
duration: 10000,
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (type === 'autopay.insufficient_funds') {
|
||
const required = message.required_rubles ?? (message.required_kopeks ?? 0) / 100;
|
||
const balance = message.balance_rubles ?? (message.balance_kopeks ?? 0) / 100;
|
||
showToast({
|
||
type: 'warning',
|
||
title: t('wsNotifications.autopay.insufficientTitle', 'Insufficient funds'),
|
||
message: t(
|
||
'wsNotifications.autopay.insufficientMessage',
|
||
'Need {{required}} {{currency}} for renewal, but balance is {{balance}} {{currency}}',
|
||
{
|
||
required: formatAmount(required),
|
||
balance: formatAmount(balance),
|
||
currency: currencySymbol,
|
||
},
|
||
),
|
||
icon: <span className="text-lg">💳</span>,
|
||
onClick: () => navigate('/balance'),
|
||
duration: 10000,
|
||
});
|
||
return;
|
||
}
|
||
|
||
// SBP recurring events
|
||
if (type === 'sbp_recurring.confirmed') {
|
||
const amount = message.amount_rubles ?? (message.amount_kopeks ?? 0) / 100;
|
||
showToast({
|
||
type: 'success',
|
||
title: t('wsNotifications.sbpRecurring.confirmedTitle', 'SBP payment confirmed'),
|
||
message: t(
|
||
'wsNotifications.sbpRecurring.confirmedMessage',
|
||
'Payment of {{amount}} {{currency}} was charged successfully',
|
||
{
|
||
amount: formatAmount(amount),
|
||
currency: currencySymbol,
|
||
},
|
||
),
|
||
icon: <span className="text-lg">🔁</span>,
|
||
onClick: () => navigate('/subscriptions'),
|
||
duration: 8000,
|
||
});
|
||
queryClient.invalidateQueries({
|
||
predicate: (query) =>
|
||
Array.isArray(query.queryKey) && query.queryKey[0] === 'subscription',
|
||
});
|
||
queryClient.invalidateQueries({
|
||
predicate: (query) =>
|
||
Array.isArray(query.queryKey) && query.queryKey[0] === 'sbp-recurring',
|
||
});
|
||
queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] });
|
||
refreshUser();
|
||
return;
|
||
}
|
||
|
||
if (type === 'sbp_recurring.activated') {
|
||
showToast({
|
||
type: 'success',
|
||
title: t('wsNotifications.sbpRecurring.activatedTitle', 'SBP auto-payment connected'),
|
||
message: t(
|
||
'wsNotifications.sbpRecurring.activatedMessage',
|
||
'SBP auto-payment has been successfully connected',
|
||
),
|
||
icon: <span className="text-lg">🔁</span>,
|
||
onClick: () => navigate('/subscriptions'),
|
||
duration: 8000,
|
||
});
|
||
queryClient.invalidateQueries({
|
||
predicate: (query) =>
|
||
Array.isArray(query.queryKey) && query.queryKey[0] === 'subscription',
|
||
});
|
||
queryClient.invalidateQueries({
|
||
predicate: (query) =>
|
||
Array.isArray(query.queryKey) && query.queryKey[0] === 'sbp-recurring',
|
||
});
|
||
queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] });
|
||
refreshUser();
|
||
return;
|
||
}
|
||
|
||
if (type === 'sbp_recurring.failed') {
|
||
showToast({
|
||
type: 'error',
|
||
title: t('wsNotifications.sbpRecurring.failedTitle', 'SBP payment failed'),
|
||
message: t(
|
||
'wsNotifications.sbpRecurring.failedMessage',
|
||
'Failed to charge your SBP auto-payment',
|
||
),
|
||
icon: <span className="text-lg">❌</span>,
|
||
onClick: () => navigate('/subscriptions'),
|
||
duration: 10000,
|
||
});
|
||
queryClient.invalidateQueries({
|
||
predicate: (query) =>
|
||
Array.isArray(query.queryKey) && query.queryKey[0] === 'subscription',
|
||
});
|
||
queryClient.invalidateQueries({
|
||
predicate: (query) =>
|
||
Array.isArray(query.queryKey) && query.queryKey[0] === 'sbp-recurring',
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (type === 'sbp_recurring.past_due') {
|
||
showToast({
|
||
type: 'warning',
|
||
title: t('wsNotifications.sbpRecurring.pastDueTitle', 'SBP payment overdue'),
|
||
message: t(
|
||
'wsNotifications.sbpRecurring.pastDueMessage',
|
||
'Your SBP auto-payment is overdue. Please check your banking app.',
|
||
),
|
||
icon: <span className="text-lg">⚠️</span>,
|
||
onClick: () => navigate('/subscriptions'),
|
||
duration: 10000,
|
||
});
|
||
queryClient.invalidateQueries({
|
||
predicate: (query) =>
|
||
Array.isArray(query.queryKey) && query.queryKey[0] === 'subscription',
|
||
});
|
||
queryClient.invalidateQueries({
|
||
predicate: (query) =>
|
||
Array.isArray(query.queryKey) && query.queryKey[0] === 'sbp-recurring',
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (type === 'sbp_recurring.cancelled') {
|
||
showToast({
|
||
type: 'warning',
|
||
title: t('wsNotifications.sbpRecurring.cancelledTitle', 'SBP auto-payment disabled'),
|
||
message: t(
|
||
'wsNotifications.sbpRecurring.cancelledMessage',
|
||
'SBP auto-payment has been disabled',
|
||
),
|
||
icon: <span className="text-lg">🔕</span>,
|
||
onClick: () => navigate('/subscriptions'),
|
||
duration: 8000,
|
||
});
|
||
queryClient.invalidateQueries({
|
||
predicate: (query) =>
|
||
Array.isArray(query.queryKey) && query.queryKey[0] === 'subscription',
|
||
});
|
||
queryClient.invalidateQueries({
|
||
predicate: (query) =>
|
||
Array.isArray(query.queryKey) && query.queryKey[0] === 'sbp-recurring',
|
||
});
|
||
return;
|
||
}
|
||
|
||
// Account events
|
||
if (type === 'account.banned') {
|
||
showToast({
|
||
type: 'error',
|
||
title: t('wsNotifications.account.bannedTitle', 'Account blocked'),
|
||
message:
|
||
message.reason ||
|
||
t('wsNotifications.account.bannedMessage', 'Your account has been blocked'),
|
||
icon: <span className="text-lg">🚫</span>,
|
||
duration: 15000,
|
||
});
|
||
refreshUser();
|
||
return;
|
||
}
|
||
|
||
if (type === 'account.unbanned') {
|
||
showToast({
|
||
type: 'success',
|
||
title: t('wsNotifications.account.unbannedTitle', 'Account unblocked'),
|
||
message: t('wsNotifications.account.unbannedMessage', 'Your account has been unblocked'),
|
||
icon: <span className="text-lg">✅</span>,
|
||
duration: 8000,
|
||
});
|
||
refreshUser();
|
||
return;
|
||
}
|
||
|
||
if (type === 'account.warning') {
|
||
showToast({
|
||
type: 'warning',
|
||
title: t('wsNotifications.account.warningTitle', 'Warning'),
|
||
message:
|
||
message.message ||
|
||
t('wsNotifications.account.warningMessage', 'You have received a warning'),
|
||
icon: <span className="text-lg">⚠️</span>,
|
||
duration: 10000,
|
||
});
|
||
return;
|
||
}
|
||
|
||
// Referral events
|
||
if (type === 'referral.bonus') {
|
||
const bonus = message.bonus_rubles ?? (message.bonus_kopeks ?? 0) / 100;
|
||
showToast({
|
||
type: 'success',
|
||
title: t('wsNotifications.referral.bonusTitle', 'Referral bonus'),
|
||
message: message.referral_name
|
||
? t(
|
||
'wsNotifications.referral.bonusWithName',
|
||
'You received {{amount}} {{currency}} bonus from {{name}}!',
|
||
{
|
||
amount: formatAmount(bonus),
|
||
currency: currencySymbol,
|
||
name: message.referral_name,
|
||
},
|
||
)
|
||
: t(
|
||
'wsNotifications.referral.bonusMessage',
|
||
'You received {{amount}} {{currency}} referral bonus!',
|
||
{
|
||
amount: formatAmount(bonus),
|
||
currency: currencySymbol,
|
||
},
|
||
),
|
||
icon: <span className="text-lg">🎁</span>,
|
||
onClick: () => navigate('/referral'),
|
||
duration: 8000,
|
||
});
|
||
queryClient.invalidateQueries({ queryKey: ['balance'] });
|
||
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
|
||
queryClient.invalidateQueries({ queryKey: ['referral-stats'] });
|
||
refreshUser();
|
||
return;
|
||
}
|
||
|
||
if (type === 'referral.registered') {
|
||
showToast({
|
||
type: 'success',
|
||
title: t('wsNotifications.referral.registeredTitle', 'New referral'),
|
||
message: message.referral_name
|
||
? t(
|
||
'wsNotifications.referral.registeredWithName',
|
||
'{{name}} joined using your referral link!',
|
||
{ name: message.referral_name },
|
||
)
|
||
: t(
|
||
'wsNotifications.referral.registeredMessage',
|
||
'Someone joined using your referral link!',
|
||
),
|
||
icon: <span className="text-lg">👤</span>,
|
||
onClick: () => navigate('/referral'),
|
||
duration: 6000,
|
||
});
|
||
queryClient.invalidateQueries({ queryKey: ['referral-stats'] });
|
||
return;
|
||
}
|
||
|
||
// Payment received
|
||
if (type === 'payment.received') {
|
||
const amount = message.amount_rubles ?? (message.amount_kopeks ?? 0) / 100;
|
||
showToast({
|
||
type: 'success',
|
||
title: t('wsNotifications.payment.receivedTitle', 'Payment received'),
|
||
message: t(
|
||
'wsNotifications.payment.receivedMessage',
|
||
'Payment of {{amount}} {{currency}} received',
|
||
{
|
||
amount: formatAmount(amount),
|
||
currency: currencySymbol,
|
||
},
|
||
),
|
||
icon: <span className="text-lg">💳</span>,
|
||
onClick: () => navigate('/balance'),
|
||
duration: 6000,
|
||
});
|
||
queryClient.invalidateQueries({ queryKey: ['balance'] });
|
||
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
|
||
queryClient.invalidateQueries({ queryKey: ['transactions'] });
|
||
refreshUser();
|
||
return;
|
||
}
|
||
},
|
||
[
|
||
t,
|
||
showToast,
|
||
showSuccessModal,
|
||
navigate,
|
||
queryClient,
|
||
refreshUser,
|
||
formatAmount,
|
||
currencySymbol,
|
||
],
|
||
);
|
||
|
||
// Connect to WebSocket and handle messages
|
||
useWebSocket({
|
||
onMessage: handleMessage,
|
||
});
|
||
|
||
// This component doesn't render anything
|
||
return null;
|
||
}
|