UX: Major TeleBot checkout flow improvements

1. Show variants immediately on product page (removed duplicate buy button)
   - Variants now display directly on product details page
   - No intermediate "select variant" button needed
   - Faster checkout flow

2. Add post-purchase prompt (Checkout or Continue Shopping)
   - After adding to basket, shows "What would you like to do next?"
   - Options: View Basket, Checkout, or Continue Shopping
   - Continue Shopping returns to the product page

3. Remove quantity +/- buttons from product details
   - Simplified to single item purchase only
   - Cleaner interface for mobile users

4. Rename "Cart" to "Basket" throughout
   - All user-facing text updated
   - "Add to Cart" → "Add to Basket"
   - "Shopping Cart" → "Shopping Basket"
   - "View Cart" → "View Basket"
   - "Clear Cart" → "Clear Basket"

Technical Changes:
- MenuBuilder.ProductDetailMenu: Now shows variant selection inline
- CallbackHandler: Updated to use ProductDetailMenu for variant updates
- Added PostAddToCartMenu for post-purchase options
- HandleAddToCart/HandleConfirmVariant: Show prompt instead of returning to product

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
SysAdmin 2025-10-06 03:26:19 +01:00
parent 1e93008df4
commit 9432ae8ccb
3 changed files with 109 additions and 86 deletions

View File

@ -494,14 +494,12 @@ namespace TeleBot.Handlers
finalQuantity finalQuantity
); );
await bot.AnswerCallbackQueryAsync( // Send new message with post-add prompt
callbackQuery.Id, await bot.SendTextMessageAsync(
$"✅ Added {finalQuantity}x {itemName} to cart", callbackQuery.Message!.Chat.Id,
showAlert: false $"✅ Added {product.Name} to basket!\n\nWhat would you like to do next?",
replyMarkup: _menuBuilder.PostAddToCartMenu(productId.Value)
); );
// Send new cart message instead of editing
await SendNewCartMessage(bot, callbackQuery.Message!.Chat.Id, session);
} }
private async Task HandleViewCart(ITelegramBotClient bot, Message message, UserSession session) private async Task HandleViewCart(ITelegramBotClient bot, Message message, UserSession session)
@ -1453,18 +1451,11 @@ namespace TeleBot.Handlers
session.TempData["selected_variants"] = selectedVariants; session.TempData["selected_variants"] = selectedVariants;
var quantity = session.TempData.ContainsKey("current_quantity") // Update the product detail menu with the selected variants
? (int)session.TempData["current_quantity"]
: 1;
var multiBuyId = session.TempData.ContainsKey("current_multibuy")
? session.TempData["current_multibuy"] as string
: null;
await bot.EditMessageReplyMarkupAsync( await bot.EditMessageReplyMarkupAsync(
message.Chat.Id, message.Chat.Id,
message.MessageId, message.MessageId,
replyMarkup: _menuBuilder.VariantSelectionMenu(product, quantity, multiBuyId, selectedVariants) replyMarkup: _menuBuilder.ProductDetailMenu(product, 1, selectedVariants)
); );
} }
@ -1523,16 +1514,13 @@ namespace TeleBot.Handlers
// Add to cart with selected variants // Add to cart with selected variants
var cartItem = session.Cart.AddItem(product, quantity.Value, multiBuyId, variantId: null, selectedVariants); var cartItem = session.Cart.AddItem(product, quantity.Value, multiBuyId, variantId: null, selectedVariants);
// Show success message // Send new message with post-add prompt
await bot.AnswerCallbackQueryAsync( await bot.SendTextMessageAsync(
callbackQuery.Id, callbackQuery.Message!.Chat.Id,
$"✅ Added {quantity.Value}x {product.Name} with {string.Join(", ", selectedVariants)} to cart!", $"✅ Added {product.Name} ({string.Join(", ", selectedVariants)}) to basket!\n\nWhat would you like to do next?",
showAlert: true replyMarkup: _menuBuilder.PostAddToCartMenu(productId.Value)
); );
// Return to product view
await HandleProductDetail(bot, callbackQuery.Message!, session, productId.Value);
// Clear temp data // Clear temp data
session.TempData.Remove("selected_variants"); session.TempData.Remove("selected_variants");
session.TempData.Remove("current_multibuy"); session.TempData.Remove("current_multibuy");

View File

@ -21,7 +21,7 @@ namespace TeleBot.UI
return new InlineKeyboardMarkup(new[] return new InlineKeyboardMarkup(new[]
{ {
new[] { InlineKeyboardButton.WithCallbackData("🛍️ Browse Products", "browse") }, new[] { InlineKeyboardButton.WithCallbackData("🛍️ Browse Products", "browse") },
new[] { InlineKeyboardButton.WithCallbackData("🛒 View Cart", "cart") }, new[] { InlineKeyboardButton.WithCallbackData("🛒 View Basket", "cart") },
new[] { InlineKeyboardButton.WithCallbackData("📦 My Orders", "orders") }, new[] { InlineKeyboardButton.WithCallbackData("📦 My Orders", "orders") },
new[] { InlineKeyboardButton.WithCallbackData("⭐ Reviews", "reviews") }, new[] { InlineKeyboardButton.WithCallbackData("⭐ Reviews", "reviews") },
new[] { InlineKeyboardButton.WithCallbackData("💬 Support", "support") }, new[] { InlineKeyboardButton.WithCallbackData("💬 Support", "support") },
@ -91,75 +91,92 @@ namespace TeleBot.UI
} }
// Navigation // Navigation
buttons.Add(new[] { InlineKeyboardButton.WithCallbackData("🛒 View Cart", "cart") }); buttons.Add(new[] { InlineKeyboardButton.WithCallbackData("🛒 View Basket", "cart") });
buttons.Add(new[] { InlineKeyboardButton.WithCallbackData("⬅️ Back", "browse") }); buttons.Add(new[] { InlineKeyboardButton.WithCallbackData("⬅️ Back", "browse") });
return new InlineKeyboardMarkup(buttons); return new InlineKeyboardMarkup(buttons);
} }
public InlineKeyboardMarkup ProductDetailMenu(Product product, int quantity = 1) public InlineKeyboardMarkup ProductDetailMenu(Product product, int quantity = 1, List<string>? selectedVariants = null)
{ {
var buttons = new List<InlineKeyboardButton[]>(); var buttons = new List<InlineKeyboardButton[]>();
selectedVariants ??= new List<string>();
// Check if product has variants // Check if product has variants
bool hasVariants = product.Variants?.Any(v => v.IsActive) == true; bool hasVariants = product.Variants?.Any(v => v.IsActive) == true;
// Show multi-buy options if available // If product has variants, show variant selection directly (no intermediate button)
if (product.MultiBuys?.Any(mb => mb.IsActive) == true) if (hasVariants)
{ {
buttons.Add(new[] { // Group variants by type
InlineKeyboardButton.WithCallbackData("💰 Multi-Buy Deals:", "noop") var variantGroups = product.Variants!
}); .Where(v => v.IsActive)
.GroupBy(v => v.VariantType)
.ToList();
foreach (var multiBuy in product.MultiBuys.Where(mb => mb.IsActive).OrderBy(mb => mb.Quantity)) // For each variant type, show options
foreach (var group in variantGroups)
{ {
var label = $"{multiBuy.Name} - £{multiBuy.Price:F2}"; buttons.Add(new[] {
if (multiBuy.Quantity > 1) InlineKeyboardButton.WithCallbackData($"Select {group.Key}:", "noop")
label += $" (£{multiBuy.PricePerUnit:F2}/each)";
// Use short callback data
var callbackData = hasVariants
? _mapper.BuildCallback("selectvar", product.Id, multiBuy.Quantity, multiBuy.Id)
: _mapper.BuildCallback("add", product.Id, multiBuy.Quantity, multiBuy.Id);
buttons.Add(new[]
{
InlineKeyboardButton.WithCallbackData(label, callbackData)
}); });
var variantButtons = new List<InlineKeyboardButton>();
foreach (var variant in group.OrderBy(v => v.SortOrder))
{
string variantInfo = "";
// Show price if different from base product price
if (variant.Price.HasValue && variant.Price.Value != product.Price)
{
variantInfo = $" - £{variant.Price.Value:F2}";
}
var isSelected = selectedVariants.Contains(variant.Name);
var buttonText = isSelected
? $"✅ {variant.Name}{variantInfo}"
: $"{variant.Name}{variantInfo}";
var callbackData = $"setvariant:{_mapper.GetShortId(product.Id, "p")}:{variant.Name}";
variantButtons.Add(InlineKeyboardButton.WithCallbackData(
buttonText,
callbackData
));
}
// Add variant buttons in rows of 3
for (int i = 0; i < variantButtons.Count; i += 3)
{
buttons.Add(variantButtons.Skip(i).Take(3).ToArray());
}
} }
// Add regular single item option // Check if all variant types have been selected
var singleCallbackData = hasVariants bool canConfirm = variantGroups.All(g => g.Any(v => selectedVariants.Contains(v.Name)));
? _mapper.BuildCallback("selectvar", product.Id, 1)
: _mapper.BuildCallback("add", product.Id, 1);
buttons.Add(new[] { if (canConfirm)
InlineKeyboardButton.WithCallbackData( {
$"🛒 BUY {product.Name} - £{product.Price:F2}", var variantString = string.Join(",", selectedVariants);
singleCallbackData var shortProductId = _mapper.GetShortId(product.Id, "p");
) var callbackData = $"confirmvar:{shortProductId}:1:null:{variantString}";
});
buttons.Add(new[] {
InlineKeyboardButton.WithCallbackData(
$"✅ Add to Basket",
callbackData
)
});
}
} }
else else
{ {
// No multi-buys, show quantity selector // No variants, simple add to basket button
var quantityButtons = new List<InlineKeyboardButton>(); var addCallbackData = _mapper.BuildCallback("add", product.Id, 1);
if (quantity > 1)
quantityButtons.Add(InlineKeyboardButton.WithCallbackData("", _mapper.BuildCallback("qty", product.Id, quantity - 1)));
quantityButtons.Add(InlineKeyboardButton.WithCallbackData($"Qty: {quantity}", "noop"));
if (quantity < 10)
quantityButtons.Add(InlineKeyboardButton.WithCallbackData("", _mapper.BuildCallback("qty", product.Id, quantity + 1)));
buttons.Add(quantityButtons.ToArray());
// Buy button
var addCallbackData = hasVariants
? _mapper.BuildCallback("selectvar", product.Id, quantity)
: _mapper.BuildCallback("add", product.Id, quantity);
buttons.Add(new[] { buttons.Add(new[] {
InlineKeyboardButton.WithCallbackData( InlineKeyboardButton.WithCallbackData(
$"🛒 BUY {product.Name} - £{product.Price * quantity:F2}", $"🛒 Add to Basket - £{product.Price:F2}",
addCallbackData addCallbackData
) )
}); });
@ -170,7 +187,25 @@ namespace TeleBot.UI
return new InlineKeyboardMarkup(buttons); return new InlineKeyboardMarkup(buttons);
} }
public InlineKeyboardMarkup PostAddToCartMenu(Guid productId)
{
var buttons = new List<InlineKeyboardButton[]>
{
new[]
{
InlineKeyboardButton.WithCallbackData("🛒 View Basket", "cart"),
InlineKeyboardButton.WithCallbackData("💳 Checkout", "checkout")
},
new[]
{
InlineKeyboardButton.WithCallbackData("◀️ Continue Shopping", _mapper.BuildCallback("product", productId))
}
};
return new InlineKeyboardMarkup(buttons);
}
public InlineKeyboardMarkup CartMenu(ShoppingCart cart) public InlineKeyboardMarkup CartMenu(ShoppingCart cart)
{ {
var buttons = new List<InlineKeyboardButton[]>(); var buttons = new List<InlineKeyboardButton[]>();
@ -193,9 +228,9 @@ namespace TeleBot.UI
InlineKeyboardButton.WithCallbackData("✅ Proceed to Checkout", "checkout") InlineKeyboardButton.WithCallbackData("✅ Proceed to Checkout", "checkout")
}); });
// Clear cart button // Clear basket button
buttons.Add(new[] { buttons.Add(new[] {
InlineKeyboardButton.WithCallbackData("🗑️ Clear Cart", "clear_cart") InlineKeyboardButton.WithCallbackData("🗑️ Clear Basket", "clear_cart")
}); });
} }
@ -379,7 +414,7 @@ namespace TeleBot.UI
: _mapper.BuildCallback("add", product.Id, quantity); : _mapper.BuildCallback("add", product.Id, quantity);
buttons.Add(new[] { buttons.Add(new[] {
InlineKeyboardButton.WithCallbackData($"🛒 Add to Cart", callbackData) InlineKeyboardButton.WithCallbackData($"🛒 Add to Basket", callbackData)
}); });
buttons.Add(new[] { InlineKeyboardButton.WithCallbackData("⬅️ Back", _mapper.BuildCallback("product", product.Id)) }); buttons.Add(new[] { InlineKeyboardButton.WithCallbackData("⬅️ Back", _mapper.BuildCallback("product", product.Id)) });
return new InlineKeyboardMarkup(buttons); return new InlineKeyboardMarkup(buttons);
@ -522,7 +557,7 @@ namespace TeleBot.UI
buttons.Add(new[] { buttons.Add(new[] {
InlineKeyboardButton.WithCallbackData( InlineKeyboardButton.WithCallbackData(
$"✅ Confirm & Add to Cart", $"✅ Confirm & Add to Basket",
callbackData callbackData
) )
}); });
@ -626,10 +661,10 @@ namespace TeleBot.UI
} }
else else
{ {
// No multi-buys, just show regular add to cart // No multi-buys, just show regular add to basket
buttons.Add(new[] buttons.Add(new[]
{ {
InlineKeyboardButton.WithCallbackData($"Add to Cart - £{product.Price:F2}", $"add:{product.Id}:{defaultQuantity}") InlineKeyboardButton.WithCallbackData($"Add to Basket - £{product.Price:F2}", $"add:{product.Id}:{defaultQuantity}")
}); });
} }
@ -669,7 +704,7 @@ namespace TeleBot.UI
{ {
new[] { new[] {
InlineKeyboardButton.WithCallbackData("⬅️ Back to Categories", "browse"), InlineKeyboardButton.WithCallbackData("⬅️ Back to Categories", "browse"),
InlineKeyboardButton.WithCallbackData("🛒 View Cart", "cart") InlineKeyboardButton.WithCallbackData("🛒 View Basket", "cart")
}, },
new[] { new[] {
InlineKeyboardButton.WithCallbackData("🏠 Main Menu", "menu") InlineKeyboardButton.WithCallbackData("🏠 Main Menu", "menu")

View File

@ -136,7 +136,7 @@ namespace TeleBot.UI
sb.AppendLine($"\n🖼 _{product.Photos.Count} photo(s) available_"); sb.AppendLine($"\n🖼 _{product.Photos.Count} photo(s) available_");
} }
sb.AppendLine("\nSelect quantity and add to cart:"); sb.AppendLine("\nSelect quantity and add to basket:");
return sb.ToString(); return sb.ToString();
} }
@ -183,7 +183,7 @@ namespace TeleBot.UI
sb.AppendLine($"\n📝 *Description:*\n{product.Description}"); sb.AppendLine($"\n📝 *Description:*\n{product.Description}");
} }
sb.AppendLine("\n*Select an option to add to cart:*"); sb.AppendLine("\n*Select an option to add to basket:*");
return sb.ToString(); return sb.ToString();
} }
@ -191,12 +191,12 @@ namespace TeleBot.UI
public static string FormatCart(ShoppingCart cart) public static string FormatCart(ShoppingCart cart)
{ {
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.AppendLine("🛒 *Shopping Cart*\n"); sb.AppendLine("🛒 *Shopping Basket*\n");
if (cart.IsEmpty()) if (cart.IsEmpty())
{ {
sb.AppendLine("Your cart is empty.\n"); sb.AppendLine("Your basket is empty.\n");
sb.AppendLine("Browse products to add items to your cart."); sb.AppendLine("Browse products to add items to your basket.");
return sb.ToString(); return sb.ToString();
} }
@ -363,13 +363,13 @@ namespace TeleBot.UI
"/start - Start shopping\n" + "/start - Start shopping\n" +
"/browse - Browse categories\n" + "/browse - Browse categories\n" +
"/products - View all products\n" + "/products - View all products\n" +
"/cart - View shopping cart\n" + "/cart - View shopping basket\n" +
"/orders - View your orders\n" + "/orders - View your orders\n" +
"/review - Review delivered products\n" + "/review - Review delivered products\n" +
"/reviews - View all product reviews\n" + "/reviews - View all product reviews\n" +
"/support - Chat with support\n" + "/support - Chat with support\n" +
"/privacy - Privacy policy\n" + "/privacy - Privacy policy\n" +
"/clear - Clear shopping cart\n" + "/clear - Clear shopping basket\n" +
"/cancel - Cancel current operation\n" + "/cancel - Cancel current operation\n" +
"/delete - Delete all your data\n" + "/delete - Delete all your data\n" +
"/help - Show this help message\n\n"; "/help - Show this help message\n\n";