diff --git a/VERSION.md b/VERSION.md
index 3c9f9e9..b2e21b7 100644
--- a/VERSION.md
+++ b/VERSION.md
@@ -9,10 +9,17 @@
## Current Version
-**v1.2.3** — 2026-08-04
+**v1.2.4** — 2026-08-05
## Changelog
+### v1.2.4 — 2026-08-05
+- **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**: answerCallbackQuery moved to start of callback handling — no stuck "clock" spinner on buttons
+- **fix**: empty city fallback in "Select district in :" (district_unknown)
+- **fix**: guard against stale underscore-format state.location in handleDistrictBack
+
### v1.2.3 — 2026-08-04
- **fix**: empty category menus — districts with no in-stock products show "No products available in this district" (getCategoriesWithProductsByLocationId filters by real stock/mono)
- **fix**: duplicate "Select your country" messages on rapid Products tap — 1200ms main-menu debounce in routes.js
diff --git a/src/admin/views/partials/app-sidebar.ejs b/src/admin/views/partials/app-sidebar.ejs
index af91c4a..93bd029 100644
--- a/src/admin/views/partials/app-sidebar.ejs
+++ b/src/admin/views/partials/app-sidebar.ejs
@@ -28,7 +28,7 @@
- v1.2.3
+ v1.2.4
@@ -43,10 +43,10 @@
- Current: v1.2.3 · 2026-08-04
+ Current: v1.2.4 · 2026-08-05
-
v1.2.3 — 2026-08-04
+
v1.2.4 — 2026-08-05
refactor Removed deposit amount-selection step; deposit_wallet_ now goes directly to Mercuryo instructions
feat Updated Mercuryo button text to include VISA/Mastercard branding in all locales
diff --git a/src/handlers/userHandlers/userProductHandler.js b/src/handlers/userHandlers/userProductHandler.js
index dd7cbaf..ceaa591 100644
--- a/src/handlers/userHandlers/userProductHandler.js
+++ b/src/handlers/userHandlers/userProductHandler.js
@@ -159,7 +159,7 @@ export default class UserProductHandler {
};
await bot.editMessageText(
- t('products.select_district', { city }),
+ t('products.select_district', { city: city || t('products.district_unknown') }),
{
chat_id: chatId,
message_id: messageId,
@@ -208,7 +208,7 @@ export default class UserProductHandler {
}
await userStates.set(chatId, {
- location: `${location.country}_${location.city}_${location.district}`,
+ location: `${encodeURIComponent(location.country)}|${encodeURIComponent(location.city)}|${encodeURIComponent(location.district)}`,
locationId: location.id
});
@@ -259,6 +259,56 @@ export default class UserProductHandler {
}
}
+ static async handleDistrictBack(callbackQuery) {
+ const chatId = callbackQuery.message.chat.id;
+ const messageId = callbackQuery.message.message_id;
+
+ try {
+ const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+
+ const state = await userStates.get(chatId);
+ if (!state?.location || !state.location.includes('|')) {
+ await bot.editMessageText(t('products.error_loading_districts'), {
+ chat_id: chatId,
+ message_id: messageId
+ });
+ return;
+ }
+
+ const [country, city] = state.location.split('|').map(decodeURIComponent);
+ const locations = await LocationService.getLocationsByCountryAndCity(country, city);
+
+ const keyboard = {
+ inline_keyboard: [
+ ...locations.map(loc => [{
+ text: loc.district || loc.city,
+ callback_data: `shop_loc_${loc.id}`
+ }]),
+ [{ text: t('products.back_to_cities'), callback_data: `shop_country_${encodeURIComponent(country)}` }]
+ ]
+ };
+
+ await bot.editMessageText(
+ t('products.select_district', { city: city || t('products.district_unknown') }),
+ {
+ chat_id: chatId,
+ message_id: messageId,
+ reply_markup: keyboard
+ }
+ );
+ } catch (error) {
+ logger.error({ err: error }, 'Error in handleDistrictBack');
+ const telegramId = callbackQuery.from.id;
+ const user = await UserService.getUserByTelegramId(telegramId).catch(() => null);
+ const lang = user?.language || 'en';
+ const t = tForUser(lang);
+ await bot.sendMessage(chatId, t('products.error_loading_districts'));
+ }
+ }
+
static async handleCategorySelection(callbackQuery) {
const chatId = callbackQuery.message.chat.id;
const messageId = callbackQuery.message.message_id;
diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json
index 38a7b59..f6dbd7f 100644
--- a/src/i18n/locales/de.json
+++ b/src/i18n/locales/de.json
@@ -60,7 +60,8 @@
"mono_product": "📦 Digitales Produkt",
"infinite_stock": "∞ Immer verfügbar",
"no_description": "Keine Beschreibung",
- "no_photo": "Kein Foto verfügbar"
+ "no_photo": "Kein Foto verfügbar",
+ "district_unknown": "Unbekannt"
},
"purchase": {
"summary": "🛒 Kaufübersicht:",
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 4d5ccf5..41fc399 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -60,7 +60,8 @@
"mono_product": "📦 Digital Product",
"infinite_stock": "∞ Always available",
"no_description": "No description provided",
- "no_photo": "No photo available"
+ "no_photo": "No photo available",
+ "district_unknown": "Unknown"
},
"purchase": {
"summary": "🛒 Purchase Summary:",
diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json
index 7716032..82b64d6 100644
--- a/src/i18n/locales/es.json
+++ b/src/i18n/locales/es.json
@@ -60,7 +60,8 @@
"mono_product": "📦 Producto Digital",
"infinite_stock": "∞ Siempre disponible",
"no_description": "Sin descripción",
- "no_photo": "Sin foto disponible"
+ "no_photo": "Sin foto disponible",
+ "district_unknown": "Desconocido"
},
"purchase": {
"summary": "🛒 Resumen de compra:",
diff --git a/src/index.js b/src/index.js
index dd8559b..adff053 100644
--- a/src/index.js
+++ b/src/index.js
@@ -72,15 +72,10 @@ if (bot && botAvailable) {
await bot.answerCallbackQuery(callbackQuery.id, { text: '⏳' }).catch(() => {});
return;
}
- let answered = false;
+ await bot.answerCallbackQuery(callbackQuery.id).catch(() => {});
try {
await callbackRouter.dispatch(callbackQuery);
- await bot.answerCallbackQuery(callbackQuery.id);
- answered = true;
} catch (error) {
- if (!answered) {
- await bot.answerCallbackQuery(callbackQuery.id).catch(() => {});
- }
await ErrorHandler.handleError(bot, callbackQuery.message.chat.id, error, 'callback query');
}
});
diff --git a/src/router/routes.js b/src/router/routes.js
index 4a8e94c..eb5a616 100644
--- a/src/router/routes.js
+++ b/src/router/routes.js
@@ -237,10 +237,18 @@ export function registerRoutes() {
logDebug(cq.data, 'handleDistrictSelection');
await userProductHandler.handleDistrictSelection(cq);
});
+ callbackRouter.registerPrefix('shop_district_', async (cq) => {
+ logDebug(cq.data, 'handleDistrictBack');
+ await userProductHandler.handleDistrictBack(cq);
+ });
callbackRouter.registerPrefix('shop_category_', async (cq) => {
logDebug(cq.data, 'handleCategorySelection');
await userProductHandler.handleCategorySelection(cq);
});
+ callbackRouter.registerPrefix('shop_subcategory_', async (cq) => {
+ logDebug(cq.data, 'handleSubcategorySelection');
+ await userProductHandler.handleSubcategorySelection(cq);
+ });
callbackRouter.registerPrefix('shop_product_', async (cq) => {
logDebug(cq.data, 'handleProductSelection');
await userProductHandler.handleProductSelection(cq);