Merge pull request #126 from BEDOLAGA-DEV/dev

Dev
This commit is contained in:
Egor
2026-01-31 20:49:27 +03:00
committed by GitHub
6 changed files with 78 additions and 3 deletions

View File

@@ -103,6 +103,11 @@ export const subscriptionApi = {
await apiClient.post('/cabinet/subscription/devices/save-cart', { devices });
},
// Save traffic cart for later purchase after top-up
saveTrafficCart: async (trafficGb: number): Promise<void> => {
await apiClient.post('/cabinet/subscription/traffic/save-cart', { traffic_gb: trafficGb });
},
// Update autopay settings
updateAutopay: async (
enabled: boolean,

View File

@@ -58,6 +58,16 @@ const DevicesIcon = () => (
</svg>
);
const TrafficIcon = () => (
<svg className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
/>
</svg>
);
const CloseIcon = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
@@ -125,6 +135,7 @@ export default function SuccessNotificationModal() {
data.type === 'subscription_renewed' ||
data.type === 'subscription_purchased';
const isDevicesPurchased = data.type === 'devices_purchased';
const isTrafficPurchased = data.type === 'traffic_purchased';
// Format amount
const formattedAmount = data.amountKopeks
@@ -173,6 +184,10 @@ export default function SuccessNotificationModal() {
title = t('successNotification.devicesPurchased.title', 'Devices added!');
icon = <DevicesIcon />;
gradientClass = 'from-blue-500 to-cyan-600';
} else if (data.type === 'traffic_purchased') {
title = t('successNotification.trafficPurchased.title', 'Traffic added!');
icon = <TrafficIcon />;
gradientClass = 'from-emerald-500 to-teal-600';
}
}
@@ -227,9 +242,9 @@ export default function SuccessNotificationModal() {
: t('successNotification.price', 'Price')}
</span>
<span
className={`text-lg font-bold ${isDevicesPurchased ? 'text-dark-100' : 'text-success-400'}`}
className={`text-lg font-bold ${isDevicesPurchased || isTrafficPurchased ? 'text-dark-100' : 'text-success-400'}`}
>
{isDevicesPurchased ? '' : '+'}
{isDevicesPurchased || isTrafficPurchased ? '' : '+'}
{formattedAmount}
</span>
</div>
@@ -254,6 +269,25 @@ export default function SuccessNotificationModal() {
</div>
)}
{/* Traffic info (for traffic purchase) */}
{isTrafficPurchased && data.trafficGbAdded && (
<div className="flex items-center justify-between rounded-xl bg-dark-800/50 px-4 py-3">
<span className="text-dark-400">
{t('successNotification.trafficAdded', 'Traffic added')}
</span>
<span className="text-lg font-bold text-emerald-400">+{data.trafficGbAdded} GB</span>
</div>
)}
{isTrafficPurchased && data.newTrafficLimitGb && (
<div className="flex items-center justify-between rounded-xl bg-dark-800/50 px-4 py-3">
<span className="text-dark-400">
{t('successNotification.totalTraffic', 'Total traffic')}
</span>
<span className="font-semibold text-dark-100">{data.newTrafficLimitGb} GB</span>
</div>
)}
{/* New balance (for top-up) */}
{isBalanceTopup && formattedBalance && (
<div className="flex items-center justify-between rounded-xl bg-dark-800/50 px-4 py-3">
@@ -314,6 +348,16 @@ export default function SuccessNotificationModal() {
</button>
)}
{isTrafficPurchased && (
<button
onClick={handleGoToSubscription}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-emerald-500 to-teal-600 py-3.5 font-bold text-white shadow-lg shadow-emerald-500/25 transition-all hover:from-emerald-400 hover:to-teal-500 active:from-emerald-600 active:to-teal-700"
>
<TrafficIcon />
<span>{t('successNotification.goToSubscription', 'Go to Subscription')}</span>
</button>
)}
<button
onClick={handleClose}
className="w-full rounded-xl bg-dark-800 py-3 font-semibold text-dark-300 transition-colors hover:bg-dark-700 hover:text-dark-100"

View File

@@ -185,6 +185,21 @@ export default function WebSocketNotifications() {
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({ queryKey: ['subscription'] });
queryClient.invalidateQueries({ queryKey: ['balance'] });
queryClient.invalidateQueries({ queryKey: ['transactions'] });
refreshUser();
return;
}
// Autopay events
if (type === 'autopay.success') {
const amount = message.amount_rubles ?? (message.amount_kopeks ?? 0) / 100;

View File

@@ -26,6 +26,9 @@ export interface WSMessage {
// Device purchase events
devices_added?: number;
new_device_limit?: number;
// Traffic purchase events
traffic_gb_added?: number;
new_traffic_limit_gb?: number;
// Autopay events
required_kopeks?: number;
required_rubles?: number;

View File

@@ -1257,6 +1257,9 @@ export default function Subscription() {
missingAmountKopeks={missingAmount}
compact
className="mb-3"
onBeforeTopUp={async () => {
await subscriptionApi.saveTrafficCart(selectedTrafficPackage);
}}
/>
)}
<button

View File

@@ -6,7 +6,8 @@ export type SuccessNotificationType =
| 'subscription_activated'
| 'subscription_renewed'
| 'subscription_purchased'
| 'devices_purchased';
| 'devices_purchased'
| 'traffic_purchased';
export interface SuccessNotificationData {
type: SuccessNotificationType;
@@ -26,6 +27,10 @@ export interface SuccessNotificationData {
devicesAdded?: number;
/** New total device limit */
newDeviceLimit?: number;
/** Traffic GB added */
trafficGbAdded?: number;
/** New total traffic limit in GB */
newTrafficLimitGb?: number;
}
interface SuccessNotificationState {