diff --git a/apps/bot/src/finance-commands.ts b/apps/bot/src/finance-commands.ts index 8a47d36..19f0a58 100644 --- a/apps/bot/src/finance-commands.ts +++ b/apps/bot/src/finance-commands.ts @@ -67,6 +67,8 @@ function isGroupChat(ctx: Context): boolean { export function createFinanceCommandsService(options: { householdConfigurationRepository: HouseholdConfigurationRepository financeServiceForHousehold: (householdId: string) => FinanceCommandService + miniAppUrl?: string + botUsername?: string }): { register: (bot: Bot) => void } { @@ -253,7 +255,28 @@ export function createFinanceCommandsService(options: { resolved.householdId ) - await ctx.reply(formatHouseholdStatus(locale, dashboard, settings.rentDueDay)) + const webAppUrl = + options.miniAppUrl && ctx.me.username + ? `${options.miniAppUrl}${options.miniAppUrl.includes('?') ? '&' : '?'}bot=${ctx.me.username}` + : options.miniAppUrl + + await ctx.reply( + formatHouseholdStatus(locale, dashboard, settings.rentDueDay), + webAppUrl + ? { + reply_markup: { + inline_keyboard: [ + [ + { + text: getBotTranslations(locale).setup.openMiniAppButton, + web_app: { url: webAppUrl } + } + ] + ] + } + } + : {} + ) } catch (error) { await ctx.reply(t.statementFailed((error as Error).message)) } diff --git a/apps/bot/src/household-setup.ts b/apps/bot/src/household-setup.ts index 6199a4e..91739fc 100644 --- a/apps/bot/src/household-setup.ts +++ b/apps/bot/src/household-setup.ts @@ -147,8 +147,11 @@ function setupKeyboard(input: { locale: BotLocale joinDeepLink: string | null bindings: readonly HouseholdTopicBindingRecord[] + miniAppUrl: string | undefined + botUsername: string | undefined }) { const t = getBotTranslations(input.locale).setup + const kt = getBotTranslations(input.locale).keyboard const configuredRoles = new Set(input.bindings.map((binding) => binding.role)) const rows: Array< Array< @@ -160,6 +163,10 @@ function setupKeyboard(input: { text: string callback_data: string } + | { + text: string + web_app: { url: string } + } > > = [] @@ -189,6 +196,19 @@ function setupKeyboard(input: { ]) } + // Add dashboard button + const webAppUrl = buildOpenMiniAppUrl(input.miniAppUrl, input.botUsername) + if (webAppUrl) { + rows.push([ + { + text: kt.dashboardButton, + web_app: { + url: webAppUrl + } + } + ]) + } + return rows.length > 0 ? { reply_markup: { @@ -234,6 +254,8 @@ function setupReply(input: { created: boolean joinDeepLink: string | null bindings: readonly HouseholdTopicBindingRecord[] + miniAppUrl: string | undefined + botUsername: string | undefined }) { const t = getBotTranslations(input.locale).setup return { @@ -250,7 +272,9 @@ function setupReply(input: { ...setupKeyboard({ locale: input.locale, joinDeepLink: input.joinDeepLink, - bindings: input.bindings + bindings: input.bindings, + miniAppUrl: input.miniAppUrl, + botUsername: input.botUsername }) } } @@ -382,6 +406,8 @@ export function registerHouseholdSetupCommands(options: { locale: BotLocale household: HouseholdTelegramChatRecord created: boolean + miniAppUrl: string | undefined + botUsername: string | undefined }) { const joinToken = await options.householdOnboardingService.ensureHouseholdJoinToken({ householdId: input.household.householdId, @@ -407,7 +433,9 @@ export function registerHouseholdSetupCommands(options: { household: input.household, created: input.created, joinDeepLink, - bindings + bindings, + miniAppUrl: input.miniAppUrl, + botUsername: input.botUsername }) } @@ -563,7 +591,9 @@ export function registerHouseholdSetupCommands(options: { ctx, locale, household: result.household, - created: result.status === 'created' + created: result.status === 'created', + miniAppUrl: options.miniAppUrl, + botUsername: ctx.me.username }) const sent = await ctx.reply( reply.text, @@ -966,7 +996,9 @@ export function registerHouseholdSetupCommands(options: { ctx, locale, household: result.household, - created: false + created: false, + miniAppUrl: options.miniAppUrl, + botUsername: ctx.me.username }) await ctx.answerCallbackQuery({ @@ -1068,7 +1100,9 @@ export function registerHouseholdSetupCommands(options: { ctx, locale, household: result.household, - created: false + created: false, + miniAppUrl: options.miniAppUrl, + botUsername: ctx.me.username }) try { @@ -1091,6 +1125,50 @@ export function registerHouseholdSetupCommands(options: { } ) } + options.bot.command(['app', 'dashboard'], async (ctx) => { + const locale = await resolveReplyLocale({ + ctx, + repository: options.householdConfigurationRepository + }) + const t = getBotTranslations(locale) + + if (!options.miniAppUrl) { + await ctx.reply(t.setup.openMiniAppUnavailable) + return + } + + await ctx.reply( + t.setup.openMiniAppFromPrivateChat, + openMiniAppReplyMarkup(locale, options.miniAppUrl, ctx.me.username) + ) + }) + + options.bot.command('keyboard', async (ctx) => { + const locale = await resolveReplyLocale({ + ctx, + repository: options.householdConfigurationRepository + }) + const t = getBotTranslations(locale) + + if (!options.miniAppUrl) { + await ctx.reply(t.setup.openMiniAppUnavailable) + return + } + + const webAppUrl = buildOpenMiniAppUrl(options.miniAppUrl, ctx.me.username) + if (!webAppUrl) { + await ctx.reply(t.setup.openMiniAppUnavailable) + return + } + + await ctx.reply(t.keyboard.enabled, { + reply_markup: { + keyboard: [[{ text: t.keyboard.dashboardButton, web_app: { url: webAppUrl } }]], + resize_keyboard: true, + is_persistent: true + } + }) + }) } function localeFromAccess(access: HouseholdMiniAppAccess, fallback: BotLocale): BotLocale { diff --git a/apps/bot/src/i18n/locales/en.ts b/apps/bot/src/i18n/locales/en.ts index d9555f7..495d0b2 100644 --- a/apps/bot/src/i18n/locales/en.ts +++ b/apps/bot/src/i18n/locales/en.ts @@ -13,7 +13,9 @@ export const enBotTranslations: BotTranslationCatalog = { join_link: 'Get a shareable link for new members to join', payment_add: 'Record your rent or utilities payment', pending_members: 'List pending household join requests', - approve_member: 'Approve a pending household member' + approve_member: 'Approve a pending household member', + app: 'Open the Kojori mini app', + keyboard: 'Toggle persistent dashboard button' }, help: { intro: 'Household bot is live.', @@ -121,6 +123,11 @@ export const enBotTranslations: BotTranslationCatalog = { joinLinkReady: (link, householdName) => `Join link for ${householdName}:\n${link}\n\nAnyone with this link can join the household. Share it carefully.` }, + keyboard: { + dashboardButton: '🏑 Dashboard', + enabled: 'Persistent dashboard button enabled.', + disabled: 'Persistent dashboard button disabled.' + }, anonymousFeedback: { title: 'Anonymous household note', cancelButton: 'Cancel', @@ -236,7 +243,7 @@ export const enBotTranslations: BotTranslationCatalog = { reminders: { utilities: (period) => `Utilities reminder for ${period}`, rentWarning: (period) => `Rent reminder for ${period}: payment is coming up soon.`, - rentDue: (period) => `Rent due reminder for ${period}: please settle payment today.`, + rentDue: (period) => `Rent is due for period ${period}. Request sent to the reminders topic.`, guidedEntryButton: 'Guided entry', copyTemplateButton: 'Copy template', openDashboardButton: 'Open dashboard', diff --git a/apps/bot/src/i18n/locales/ru.ts b/apps/bot/src/i18n/locales/ru.ts index 0a66bfb..30114a1 100644 --- a/apps/bot/src/i18n/locales/ru.ts +++ b/apps/bot/src/i18n/locales/ru.ts @@ -13,7 +13,9 @@ export const ruBotTranslations: BotTranslationCatalog = { join_link: 'ΠŸΠΎΠ»ΡƒΡ‡ΠΈΡ‚ΡŒ ссылку для ΠΏΡ€ΠΈΠ³Π»Π°ΡˆΠ΅Π½ΠΈΡ Π½ΠΎΠ²Ρ‹Ρ… участников', payment_add: 'ΠŸΠΎΠ΄Ρ‚Π²Π΅Ρ€Π΄ΠΈΡ‚ΡŒ ΠΎΠΏΠ»Π°Ρ‚Ρƒ Π°Ρ€Π΅Π½Π΄Ρ‹ ΠΈΠ»ΠΈ ΠΊΠΎΠΌΠΌΡƒΠ½Π°Π»ΠΊΠΈ', pending_members: 'ΠŸΠΎΠΊΠ°Π·Π°Ρ‚ΡŒ ΠΎΠΆΠΈΠ΄Π°ΡŽΡ‰ΠΈΠ΅ заявки Π½Π° вступлСниС', - approve_member: 'ΠŸΠΎΠ΄Ρ‚Π²Π΅Ρ€Π΄ΠΈΡ‚ΡŒ участника Π΄ΠΎΠΌΠ°' + approve_member: 'ΠŸΠΎΠ΄Ρ‚Π²Π΅Ρ€Π΄ΠΈΡ‚ΡŒ участника Π΄ΠΎΠΌΠ°', + app: 'ΠžΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ ΠΌΠΈΠ½ΠΈ-ΠΏΡ€ΠΈΠ»ΠΎΠΆΠ΅Π½ΠΈΠ΅ Kojori', + keyboard: 'Π’ΠΊΠ»/Π²Ρ‹ΠΊΠ» ΠΊΠ½ΠΎΠΏΠΊΡƒ Π΄Π°ΡˆΠ±ΠΎΡ€Π΄Π°' }, help: { intro: 'Π‘ΠΎΡ‚ для Π΄ΠΎΠΌΠ° ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½.', @@ -123,6 +125,11 @@ export const ruBotTranslations: BotTranslationCatalog = { joinLinkReady: (link, householdName) => `ΠŸΠΎΠ΄Π΅Π»ΠΈΡ‚Π΅ΡΡŒ этой ссылкой, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΏΡ€ΠΈΠ³Π»Π°ΡΠΈΡ‚ΡŒ участников Π² ${householdName}:\n\n${link}\n\nΠ›ΡŽΠ±ΠΎΠΉ, Ρƒ ΠΊΠΎΠ³ΠΎ Π΅ΡΡ‚ΡŒ эта ссылка, ΠΌΠΎΠΆΠ΅Ρ‚ ΠΏΠΎΠ΄Π°Ρ‚ΡŒ заявку Π½Π° вступлСниС.` }, + keyboard: { + dashboardButton: '🏑 Π”Π°ΡˆΠ±ΠΎΡ€Π΄', + enabled: 'Кнопка Π΄Π°ΡˆΠ±ΠΎΡ€Π΄Π° Π²ΠΊΠ»ΡŽΡ‡Π΅Π½Π°.', + disabled: 'Кнопка Π΄Π°ΡˆΠ±ΠΎΡ€Π΄Π° Π²Ρ‹ΠΊΠ»ΡŽΡ‡Π΅Π½Π°.' + }, anonymousFeedback: { title: 'АнонимноС сообщСниС ΠΏΠΎ Π΄ΠΎΠΌΡƒ', cancelButton: 'ΠžΡ‚ΠΌΠ΅Π½ΠΈΡ‚ΡŒ', diff --git a/apps/bot/src/i18n/types.ts b/apps/bot/src/i18n/types.ts index 0f0b969..9223f42 100644 --- a/apps/bot/src/i18n/types.ts +++ b/apps/bot/src/i18n/types.ts @@ -12,6 +12,8 @@ export type TelegramCommandName = | 'payment_add' | 'pending_members' | 'approve_member' + | 'app' + | 'keyboard' export interface BotCommandDescriptions { help: string @@ -25,6 +27,8 @@ export interface BotCommandDescriptions { payment_add: string pending_members: string approve_member: string + app: string + keyboard: string } export interface PendingMemberSummary { @@ -107,6 +111,11 @@ export interface BotTranslationCatalog { joinLinkUnavailable: string joinLinkReady: (link: string, householdName: string) => string } + keyboard: { + dashboardButton: string + enabled: string + disabled: string + } anonymousFeedback: { title: string cancelButton: string diff --git a/apps/bot/src/reminder-jobs.ts b/apps/bot/src/reminder-jobs.ts index 13928ea..78d50cd 100644 --- a/apps/bot/src/reminder-jobs.ts +++ b/apps/bot/src/reminder-jobs.ts @@ -125,11 +125,35 @@ export function createReminderJobsHandler(options: { } case 'rent-warning': return { - text: t.rentWarning(period) + text: t.rentWarning(period), + replyMarkup: { + inline_keyboard: [ + [ + { + text: t.openDashboardButton, + url: options.botUsername + ? `https://t.me/${options.botUsername}?start=dashboard` + : '#' + } + ] + ] + } } case 'rent-due': return { - text: t.rentDue(period) + text: t.rentDue(period), + replyMarkup: { + inline_keyboard: [ + [ + { + text: t.openDashboardButton, + url: options.botUsername + ? `https://t.me/${options.botUsername}?start=dashboard` + : '#' + } + ] + ] + } } } }