feat: gift subscription redesign — code-only purchase + 3-tab UI

- Remove recipient input from buy flow (code-only gifts)
- 3-tab UI: Buy, Activate, My Gifts with animations
- After purchase, switch to My Gifts tab to show gift code
- Gift code display with copy/share buttons
- Activate tab: enter gift code to activate subscription
- My Gifts tab: sent/received gifts with status badges
- Fix: use navigator.language instead of hardcoded ru-RU for dates
- Fix: spread array before sort to prevent React Query cache mutation
- Fix: ARIA tab linkage with proper id/aria-labelledby
- Fix: guard VITE_BOT_USERNAME against undefined in share URLs
- Fix: wrap clipboard/share fallbacks in try/catch
- Add error state to MyGiftsTabContent
This commit is contained in:
Fringg
2026-03-10 05:37:18 +03:00
parent d946c83357
commit af3e535c69
4 changed files with 1018 additions and 560 deletions

View File

@@ -45,8 +45,8 @@ export interface GiftConfig {
export interface GiftPurchaseRequest {
tariff_id: number;
period_days: number;
recipient_type: 'email' | 'telegram';
recipient_value: string;
recipient_type?: 'email' | 'telegram';
recipient_value?: string;
gift_message?: string;
payment_mode: 'balance' | 'gateway';
payment_method?: string;
@@ -86,6 +86,35 @@ export interface PendingGift {
created_at: string | null;
}
export interface SentGift {
token: string;
tariff_name: string | null;
period_days: number;
device_limit: number;
status: string;
gift_recipient_value: string | null;
gift_message: string | null;
activated_by_username: string | null;
created_at: string | null;
}
export interface ReceivedGift {
token: string;
tariff_name: string | null;
period_days: number;
device_limit: number;
status: string;
sender_display: string | null;
gift_message: string | null;
created_at: string | null;
}
export interface ActivateGiftResponse {
status: string;
tariff_name: string | null;
period_days: number | null;
}
// API
export const giftApi = {
@@ -108,4 +137,19 @@ export const giftApi = {
const { data } = await apiClient.get<PendingGift[]>('/cabinet/gift/pending');
return data;
},
getSentGifts: async (): Promise<SentGift[]> => {
const { data } = await apiClient.get<SentGift[]>('/cabinet/gift/sent');
return data;
},
getReceivedGifts: async (): Promise<ReceivedGift[]> => {
const { data } = await apiClient.get<ReceivedGift[]>('/cabinet/gift/received');
return data;
},
activateGiftCode: async (code: string): Promise<ActivateGiftResponse> => {
const { data } = await apiClient.post<ActivateGiftResponse>('/cabinet/gift/activate', { code });
return data;
},
};

View File

@@ -4201,6 +4201,41 @@
},
"warning": {
"telegram_unresolvable": "Could not verify this Telegram username. The recipient may not receive a notification about the gift."
}
},
"pageTitle": "Gifts",
"tabBuy": "Buy",
"tabActivate": "Activate",
"tabMyGifts": "My Gifts",
"selectTariff": "SELECT TARIFF",
"selectPeriod": "SUBSCRIPTION PERIOD",
"deviceCount": "{{count}} device",
"deviceCount_other": "{{count}} devices",
"activateTitle": "Enter gift code",
"activateDescription": "Enter the gift code you received from a friend",
"activateCodePlaceholder": "GIFT-XXXXXXXXXXXX",
"activateButton": "Activate gift",
"activating": "Activating...",
"activateSuccess": "Gift activated!",
"activateSuccessDesc": "{{tariff}} — {{days}} days added to your subscription",
"activateError": "Failed to activate gift",
"myGiftsEmpty": "No gifts yet",
"myGiftsEmptyDesc": "Buy a gift for a friend or activate a received code",
"sentGiftsTitle": "Sent",
"receivedGiftsTitle": "Received",
"statusAvailable": "AVAILABLE",
"statusActivated": "ACTIVATED",
"statusPending": "PENDING",
"statusDelivered": "DELIVERED",
"statusFailed": "FAILED",
"statusExpired": "EXPIRED",
"statusPendingActivation": "PENDING ACTIVATION",
"copyCode": "COPY",
"codeCopied": "Code copied!",
"shareGift": "SHARE",
"shareText": "I have a gift for you! Activate it here:",
"activatedBy": "Activated by {{username}}",
"sentTo": "Sent to {{recipient}}",
"daysShort": "days",
"devicesShort": "dev."
}
}

View File

@@ -4763,6 +4763,41 @@
},
"warning": {
"telegram_unresolvable": "Не удалось проверить этот Telegram-юзернейм. Получатель может не получить уведомление о подарке."
}
},
"pageTitle": "Подарки",
"tabBuy": "Купить",
"tabActivate": "Активировать",
"tabMyGifts": "Мои подарки",
"selectTariff": "ВЫБЕРИТЕ ТАРИФ",
"selectPeriod": "ПЕРИОД ПОДПИСКИ",
"deviceCount": "{{count}} устройство",
"deviceCount_other": "{{count}} устройств",
"activateTitle": "Введите код подарка",
"activateDescription": "Введите код подарка, полученный от друга или знакомого",
"activateCodePlaceholder": "GIFT-XXXXXXXXXXXX",
"activateButton": "Активировать подарок",
"activating": "Активация...",
"activateSuccess": "Подарок активирован!",
"activateSuccessDesc": "{{tariff}} — {{days}} дн. добавлено к вашей подписке",
"activateError": "Не удалось активировать подарок",
"myGiftsEmpty": "У вас пока нет подарков",
"myGiftsEmptyDesc": "Купите подарок для друга или активируйте полученный код",
"sentGiftsTitle": "Отправленные",
"receivedGiftsTitle": "Полученные",
"statusAvailable": "ДОСТУПЕН",
"statusActivated": "АКТИВИРОВАН",
"statusPending": "ОЖИДАЕТ",
"statusDelivered": "ДОСТАВЛЕН",
"statusFailed": "ОШИБКА",
"statusExpired": "ПРОСРОЧЕН",
"statusPendingActivation": "ОЖИДАЕТ АКТИВАЦИИ",
"copyCode": "КОПИРОВАТЬ",
"codeCopied": "Код скопирован!",
"shareGift": "ПОДЕЛИТЬСЯ",
"shareText": "У меня есть подарок для тебя! Активируй его здесь:",
"activatedBy": "Активирован пользователем {{username}}",
"sentTo": "Отправлен {{recipient}}",
"daysShort": "дн.",
"devicesShort": "устр."
}
}

File diff suppressed because it is too large Load Diff