fix(bot): issue #130 — crypto deposit photo-edit 400, chat cleanup via lastInlineMessageId

- depositHandler: handleDepositInstruction + handleDepositSelectWallet use editOrSendCallback (photo-safe fallback) — fixes 'no text in the message to edit' 400
- resetUserContext deletes lastInlineMessageId (stale inline menus cleaned on Reply Keyboard nav)
- showProducts/showProfile/showBalance/showPurchases store lastInlineMessageId on sendMessage
- answerCallbackQuery verified before dispatch (v1.2.4)
- 48 tests pass; bump v1.2.5
This commit is contained in:
NW
2026-08-05 12:20:16 +01:00
parent f90af4c78e
commit 61c7a90aeb
8 changed files with 32 additions and 33 deletions

View File

@@ -9,10 +9,15 @@
## Current Version ## Current Version
**v1.2.4** — 2026-08-05 **v1.2.5** — 2026-08-05
## Changelog ## Changelog
### v1.2.5 — 2026-08-05
- **fix**: crypto deposit error — handleDepositInstruction/handleDepositSelectWallet use editOrSendCallback (photo-safe fallback, fixes "no text in the message to edit" 400)
- **fix**: chat clutter — lastInlineMessageId tracked in showProducts/showProfile/showBalance/showPurchases and deleted in resetUserContext
- **verify**: answerCallbackQuery before dispatch in all callback paths (v1.2.4)
### v1.2.4 — 2026-08-05 ### v1.2.4 — 2026-08-05
- **fix**: missing `shop_district_` / `shop_subcategory_` callback handlers — Back button in empty categories now works (handleDistrictBack) - **fix**: missing `shop_district_` / `shop_subcategory_` callback handlers — Back button in empty categories now works (handleDistrictBack)
- **fix**: state.location pipe-delimited (encodeURIComponent) — multi-word names (Saint Petersburg) no longer break navigation - **fix**: state.location pipe-delimited (encodeURIComponent) — multi-word names (Saint Petersburg) no longer break navigation

View File

@@ -28,7 +28,7 @@
<svg class="sa-icon sa-thin"> <svg class="sa-icon sa-thin">
<use href="/icons/sprite.svg#tag"></use> <use href="/icons/sprite.svg#tag"></use>
</svg> </svg>
<span class="fs-xs opacity-70">v1.2.4</span> <span class="fs-xs opacity-70">v1.2.5</span>
</div> </div>
</div> </div>
</aside> </aside>
@@ -43,10 +43,10 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="alert alert-info mb-3"> <div class="alert alert-info mb-3">
<strong>Current:</strong> v1.2.4 &middot; 2026-08-05 <strong>Current:</strong> v1.2.5 &middot; 2026-08-05
</div> </div>
<h6 class="fw-bold mb-2">v1.2.4 <span class="text-muted fs-sm">&mdash; 2026-08-05</span></h6> <h6 class="fw-bold mb-2">v1.2.5 <span class="text-muted fs-sm">&mdash; 2026-08-05</span></h6>
<ul class="small mb-3"> <ul class="small mb-3">
<li><span class="badge bg-info">refactor</span> Removed deposit amount-selection step; deposit_wallet_ now goes directly to Mercuryo instructions</li> <li><span class="badge bg-info">refactor</span> Removed deposit amount-selection step; deposit_wallet_ now goes directly to Mercuryo instructions</li>
<li><span class="badge bg-primary">feat</span> Updated Mercuryo button text to include VISA/Mastercard branding in all locales</li> <li><span class="badge bg-primary">feat</span> Updated Mercuryo button text to include VISA/Mastercard branding in all locales</li>

View File

@@ -6,6 +6,7 @@ import UserService from "../../services/userService.js";
import WalletService from "../../services/walletService.js"; import WalletService from "../../services/walletService.js";
import logger from "../../utils/logger.js"; import logger from "../../utils/logger.js";
import { resetUserContext } from "../../utils/messageUtils.js"; import { resetUserContext } from "../../utils/messageUtils.js";
import userStates from "../../context/userStates.js";
import { tForUser, LANGUAGE_NAMES, AVAILABLE_LANGUAGES } from '../../i18n/index.js'; import { tForUser, LANGUAGE_NAMES, AVAILABLE_LANGUAGES } from '../../i18n/index.js';
export default class UserHandler { export default class UserHandler {
@@ -85,10 +86,11 @@ ${t('profile.member_since')}: ${new Date(userStats.created_at).toLocaleDateStrin
] ]
}; };
await bot.sendMessage(chatId, text, { const result = await bot.sendMessage(chatId, text, {
parse_mode: 'Markdown', parse_mode: 'Markdown',
reply_markup: keyboard reply_markup: keyboard
}); });
await userStates.set(chatId, { ...(await userStates.get(chatId) || {}), lastInlineMessageId: result.message_id });
} catch (error) { } catch (error) {
logger.error({ err: error }, 'Error in showProfile'); logger.error({ err: error }, 'Error in showProfile');
await bot.sendMessage(chatId, t('profile.error_loading')); await bot.sendMessage(chatId, t('profile.error_loading'));

View File

@@ -82,7 +82,8 @@ export default class UserProductHandler {
}); });
} }
} catch (error) { } catch (error) {
await bot.sendMessage(chatId, message, {reply_markup: keyboard}); const result = await bot.sendMessage(chatId, message, {reply_markup: keyboard});
await userStates.set(chatId, { ...(await userStates.get(chatId) || {}), lastInlineMessageId: result.message_id });
} }
} catch (error) { } catch (error) {
logger.error({ err: error }, 'Error in showProducts'); logger.error({ err: error }, 'Error in showProducts');

View File

@@ -159,7 +159,8 @@ export default class UserPurchaseHandler {
const { text, markup } = await this.viewPurchasePage(user.id, 0, t); const { text, markup } = await this.viewPurchasePage(user.id, 0, t);
await bot.sendMessage(chatId, text, { reply_markup: markup, parse_mode: 'Markdown' }); const result = await bot.sendMessage(chatId, text, { reply_markup: markup, parse_mode: 'Markdown' });
await userStates.set(chatId, { ...(await userStates.get(chatId) || {}), lastInlineMessageId: result.message_id });
} catch (error) { } catch (error) {
logger.error({ err: error }, 'Error in showPurchases'); logger.error({ err: error }, 'Error in showPurchases');
const t = tForUser('en'); const t = tForUser('en');

View File

@@ -5,6 +5,7 @@ import WalletService from '../../../services/walletService.js';
import bot from '../../../context/bot.js'; import bot from '../../../context/bot.js';
import logger from '../../../utils/logger.js'; import logger from '../../../utils/logger.js';
import { editOrSendCallback } from '../../../utils/messageUtils.js'; import { editOrSendCallback } from '../../../utils/messageUtils.js';
import userStates from '../../../context/userStates.js';
import { tForUser } from '../../../i18n/index.js'; import { tForUser } from '../../../i18n/index.js';
export default class BalanceHandler { export default class BalanceHandler {
@@ -86,7 +87,8 @@ export default class BalanceHandler {
{ text: t('wallet.transaction_history'), callback_data: 'view_transaction_history_0' } { text: t('wallet.transaction_history'), callback_data: 'view_transaction_history_0' }
]); ]);
await bot.sendMessage(chatId, message, { reply_markup: keyboard, parse_mode: 'Markdown' }); const result = await bot.sendMessage(chatId, message, { reply_markup: keyboard, parse_mode: 'Markdown' });
await userStates.set(chatId, { ...(await userStates.get(chatId) || {}), lastInlineMessageId: result.message_id });
} catch (error) { } catch (error) {
logger.error({ err: error }, 'Error in showBalance'); logger.error({ err: error }, 'Error in showBalance');
await bot.sendMessage(chatId, t('wallet.error_loading_balance')); await bot.sendMessage(chatId, t('wallet.error_loading_balance'));

View File

@@ -35,19 +35,14 @@ export default class DepositHandler {
); );
if (cryptoWallets.length === 0) { if (cryptoWallets.length === 0) {
await bot.editMessageText( await editOrSendCallback(callbackQuery, t('wallet.no_wallets_prefix'), {
t('wallet.no_wallets_prefix'), reply_markup: {
{ inline_keyboard: [
chat_id: chatId, [{ text: t('purchase.add_wallet'), callback_data: 'add_wallet' }],
message_id: callbackQuery.message.message_id, [{ text: t('wallet.back'), callback_data: 'back_to_balance' }]
reply_markup: { ]
inline_keyboard: [
[{ text: t('purchase.add_wallet'), callback_data: 'add_wallet' }],
[{ text: t('wallet.back'), callback_data: 'back_to_balance' }]
]
}
} }
); });
return; return;
} }
@@ -61,15 +56,10 @@ export default class DepositHandler {
walletButtons.push([{ text: t('wallet.back'), callback_data: 'back_to_balance' }]); walletButtons.push([{ text: t('wallet.back'), callback_data: 'back_to_balance' }]);
await bot.editMessageText( await editOrSendCallback(callbackQuery, t('wallet.deposit_select_gateway'), {
t('wallet.deposit_select_gateway'), parse_mode: 'Markdown',
{ reply_markup: { inline_keyboard: walletButtons }
chat_id: chatId, });
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown',
reply_markup: { inline_keyboard: walletButtons }
}
);
} catch (error) { } catch (error) {
logger.error({ err: error }, 'Error in handleDepositSelectWallet'); logger.error({ err: error }, 'Error in handleDepositSelectWallet');
await editOrSendCallback(callbackQuery, t('wallet.error_loading')); await editOrSendCallback(callbackQuery, t('wallet.error_loading'));
@@ -155,9 +145,7 @@ export default class DepositHandler {
] ]
}; };
await bot.editMessageText(message, { await editOrSendCallback(callbackQuery, message, {
chat_id: chatId,
message_id: callbackQuery.message.message_id,
parse_mode: 'Markdown', parse_mode: 'Markdown',
reply_markup: keyboard reply_markup: keyboard
}); });

View File

@@ -5,7 +5,7 @@ export async function resetUserContext(chatId) {
try { try {
const state = await userStates.get(chatId); const state = await userStates.get(chatId);
if (state) { if (state) {
const msgIds = [state.photoMessageId, state.productMessageId, state.hiddenPhotoMessageId].filter(Boolean); const msgIds = [state.photoMessageId, state.productMessageId, state.hiddenPhotoMessageId, state.lastInlineMessageId].filter(Boolean);
for (const msgId of msgIds) { for (const msgId of msgIds) {
try { await bot.deleteMessage(chatId, msgId); } catch (_) {} try { await bot.deleteMessage(chatId, msgId); } catch (_) {}
} }