feat(subscriptions): enhance subscription flow and localization updates

- Updated default locale to Russian for improved user experience.
- Refactored subscription messages to include expiration dates and active subscription status.
- Enhanced keyboard display for subscription options with clear expiration information.
- Improved handling of subscription-related queries and responses for better clarity.
This commit is contained in:
vchikalkin 2025-09-17 13:22:02 +03:00
parent e6c823570c
commit 2de018b8d4
4 changed files with 64 additions and 19 deletions

View File

@ -95,7 +95,7 @@ err-limit-exceeded = 🚫 Слишком много запросов! Подож
# Сообщения о доступе
msg-subscribe =
👑 Pro доступ:
👑 Pro доступ
• Разблокирует неограниченное количество заказов
msg-subscribe-success = ✅ Платеж успешно обработан!
msg-subscribe-error = ❌ Произошла ошибка при обработке платежа

View File

@ -18,7 +18,12 @@ export async function subscription(conversation: Conversation<Context, Context>,
const subscriptionsService = new SubscriptionsService({ telegramId });
const { remainingDays, usedTrialSubscription } = await subscriptionsService.getSubscription({
const {
hasActiveSubscription,
remainingDays,
subscription: currentSubscription,
usedTrialSubscription,
} = await subscriptionsService.getSubscription({
telegramId,
});
@ -35,19 +40,28 @@ export async function subscription(conversation: Conversation<Context, Context>,
const prices = sift(subscriptionPrices);
// строим клавиатуру
const keyboard = buildPricesKeyboard(prices);
// строим клавиатуру с указанием даты окончания после покупки
const keyboard = buildPricesKeyboard(prices, currentSubscription?.expiresAt);
// сообщение с выбором плана
const messageWithPrices = await ctx.reply(
combine(
await conversation.external(({ t }) =>
combine(
t('msg-subscribe'),
remainingDays ? t('msg-subscription-active-days', { days: remainingDays }) : '',
fmt`${i}${t('msg-cancel-operation')}${i}`.text,
),
),
await conversation.external(({ t }) => {
let statusLine = t('msg-subscribe');
if (hasActiveSubscription && currentSubscription?.expiresAt) {
statusLine = t('msg-subscription-active-until', {
date: new Date(currentSubscription.expiresAt).toLocaleDateString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
}),
});
} else if (remainingDays) {
statusLine = t('msg-subscription-active-days', { days: remainingDays });
}
return combine(statusLine, fmt`${i}${t('msg-cancel-operation')}${i}`.text);
}),
),
{ reply_markup: keyboard },
);
@ -68,16 +82,28 @@ export async function subscription(conversation: Conversation<Context, Context>,
const selectedPrice = prices.find((price) => price?.period === selectedPeriod);
if (!selectedPrice) return replyError(ctx, conversation);
// создаём invoice
// создаём invoice (с указанием даты, до которой будет доступ)
const baseDate = currentSubscription?.expiresAt
? new Date(Math.max(Date.now(), new Date(currentSubscription.expiresAt).getTime()))
: new Date();
const targetDate = addDays(baseDate, selectedPrice.days ?? 0);
const targetDateRu = targetDate.toLocaleDateString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
});
return ctx.replyWithInvoice(
'Оплата Pro доступа',
selectedPrice.description || '',
combine(
`${selectedPrice.description || 'Pro доступ'} — до ${targetDateRu}`,
'(Автопродление отключено)',
),
JSON.stringify({ period: selectedPrice.period }),
'RUB',
[
{
amount: selectedPrice.amount * 100, // Telegram ждёт в копейках
label: selectedPrice.description || 'К оплате',
label: `${selectedPrice.description || 'К оплате'} — до ${targetDateRu}`,
},
],
{
@ -89,13 +115,31 @@ export async function subscription(conversation: Conversation<Context, Context>,
// --- helpers ---
function buildPricesKeyboard(prices: GQL.SubscriptionPriceFieldsFragment[]) {
function addDays(date: Date, days: number) {
const d = new Date(date);
d.setDate(d.getDate() + days);
return d;
}
function buildPricesKeyboard(
prices: GQL.SubscriptionPriceFieldsFragment[],
currentExpiresAt?: string,
) {
const keyboard = new InlineKeyboard();
const baseTime = currentExpiresAt
? Math.max(Date.now(), new Date(currentExpiresAt).getTime())
: Date.now();
for (const price of prices) {
const targetDate = addDays(new Date(baseTime), price.days ?? 0);
const targetDateRu = targetDate.toLocaleDateString('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
});
keyboard.row({
callback_data: price.period,
pay: true,
text: `${price.description} (${formatMoney(price.amount)})`,
text: `Продлить до ${targetDateRu} (${formatMoney(price.amount)})`,
});
}

View File

@ -8,6 +8,7 @@ const composer = new Composer<Context>();
// Telegram требует отвечать на pre_checkout_query
composer.on('pre_checkout_query', logHandle('pre-checkout-query'), async (ctx) => {
console.log('🚀 ~ ctx:', ctx);
await ctx.answerPreCheckoutQuery(true);
});
@ -22,9 +23,9 @@ feature.command('subscribe', logHandle('command-subscribe'), async (ctx) => {
const proEnabled = subscriptionSetting?.proEnabled;
if (!proEnabled) return await ctx.reply(ctx.t('msg-subscribe-disabled'));
if (!proEnabled) return ctx.reply(ctx.t('msg-subscribe-disabled'));
await ctx.conversation.enter('subscription');
return ctx.conversation.enter('subscription');
});
// успешная оплата

View File

@ -3,7 +3,7 @@ import { I18n } from '@grammyjs/i18n';
import path from 'node:path';
export const i18n = new I18n<Context>({
defaultLocale: 'en',
defaultLocale: 'ru',
directory: path.resolve(process.cwd(), 'locales'),
fluentBundleOptions: {
useIsolating: false,