Feature: Add postal address memory with user preference

- Added SavedShippingAddress model to store user addresses
- Added Privacy.SaveShippingAddress preference (null = not asked, true/false = user choice)
- Enhanced checkout flow to offer using saved address if available
- Ask once about saving address, respect user's choice going forward
- Automatically save/not save based on user preference in future orders
- Added menu options: Use Saved Address, Enter New Address, Save Address (Yes/No)
- Enhanced CallbackHandler with save address handlers
- Updated MessageHandler to prompt for save preference on first use

User will only be asked once about saving addresses. Account reset clears preference.
This commit is contained in:
SysAdmin 2025-10-06 00:38:47 +01:00
parent 147e96a084
commit c8f22c783d
4 changed files with 215 additions and 17 deletions

View File

@ -140,6 +140,22 @@ namespace TeleBot.Handlers
await HandleCheckout(bot, callbackQuery.Message, session);
break;
case "use_saved_address":
await HandleUseSavedAddress(bot, callbackQuery.Message, session);
break;
case "enter_new_address":
await HandleEnterNewAddress(bot, callbackQuery.Message, session);
break;
case "save_address_yes":
await HandleSaveAddressPreference(bot, callbackQuery.Message, session, true);
break;
case "save_address_no":
await HandleSaveAddressPreference(bot, callbackQuery.Message, session, false);
break;
case "confirm_order":
await HandleConfirmOrder(bot, callbackQuery.Message, session, callbackQuery.From);
break;
@ -746,6 +762,27 @@ namespace TeleBot.Handlers
session.State = SessionState.CheckoutFlow;
// Check if user has saved address
if (session.SavedAddress != null && !string.IsNullOrWhiteSpace(session.SavedAddress.Name))
{
// Offer to use saved address
var savedAddressPreview = $"{session.SavedAddress.Name}\n" +
$"{session.SavedAddress.Address}\n" +
$"{session.SavedAddress.City}\n" +
$"{session.SavedAddress.PostCode}\n" +
$"{session.SavedAddress.Country}";
await bot.SendTextMessageAsync(
message.Chat.Id,
$"📦 *Checkout - Delivery Details*\n\n" +
$"Use your saved address?\n\n" +
$"```\n{savedAddressPreview}\n```",
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: _menuBuilder.UseSavedAddressMenu()
);
}
else
{
// Send new message for checkout - collect all details at once
await bot.SendTextMessageAsync(
message.Chat.Id,
@ -765,6 +802,92 @@ namespace TeleBot.Handlers
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown
);
}
}
private async Task HandleUseSavedAddress(ITelegramBotClient bot, Message message, UserSession session)
{
if (session.SavedAddress == null || session.OrderFlow == null)
{
await HandleEnterNewAddress(bot, message, session);
return;
}
// Copy saved address to order flow
session.OrderFlow.ShippingName = session.SavedAddress.Name;
session.OrderFlow.ShippingAddress = session.SavedAddress.Address;
session.OrderFlow.ShippingCity = session.SavedAddress.City;
session.OrderFlow.ShippingPostCode = session.SavedAddress.PostCode;
session.OrderFlow.ShippingCountry = session.SavedAddress.Country;
session.OrderFlow.CurrentStep = OrderFlowStep.ReviewingOrder;
// Show order summary
var summary = MessageFormatter.FormatOrderSummary(session.OrderFlow, session.Cart);
await bot.EditMessageTextAsync(
message.Chat.Id,
message.MessageId,
summary,
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.CheckoutConfirmMenu()
);
}
private async Task HandleEnterNewAddress(ITelegramBotClient bot, Message message, UserSession session)
{
await bot.EditMessageTextAsync(
message.Chat.Id,
message.MessageId,
"📦 *Checkout - Delivery Details*\n\n" +
"Please provide all delivery details in one message:\n\n" +
"• Full Name\n" +
"• Street Address\n" +
"• City\n" +
"• Post/Zip Code\n" +
"• Country (or leave blank for UK)\n\n" +
"_Example:_\n" +
"`John Smith\n" +
"123 Main Street\n" +
"London\n" +
"SW1A 1AA\n" +
"United Kingdom`",
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown
);
}
private async Task HandleSaveAddressPreference(ITelegramBotClient bot, Message message, UserSession session, bool saveAddress)
{
// Save user preference
session.Privacy.SaveShippingAddress = saveAddress;
if (saveAddress && session.OrderFlow != null)
{
// Save the address
session.SavedAddress = new SavedShippingAddress
{
Name = session.OrderFlow.ShippingName,
Address = session.OrderFlow.ShippingAddress,
City = session.OrderFlow.ShippingCity,
PostCode = session.OrderFlow.ShippingPostCode,
Country = session.OrderFlow.ShippingCountry,
SavedAt = DateTime.UtcNow
};
}
// Continue to order summary
if (session.OrderFlow != null)
{
session.OrderFlow.CurrentStep = OrderFlowStep.ReviewingOrder;
var summary = MessageFormatter.FormatOrderSummary(session.OrderFlow, session.Cart);
await bot.EditMessageTextAsync(
message.Chat.Id,
message.MessageId,
summary,
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.CheckoutConfirmMenu()
);
}
}
private async Task HandleConfirmOrder(ITelegramBotClient bot, Message message, UserSession session, User telegramUser)
{

View File

@ -132,6 +132,35 @@ namespace TeleBot.Handlers
? lines[4]
: "United Kingdom";
// Check if we need to ask about saving address
if (session.Privacy.SaveShippingAddress == null)
{
// Haven't asked yet - offer to save
await bot.SendTextMessageAsync(
message.Chat.Id,
"💾 *Save Address*\n\n" +
"Would you like to save this address for future orders?\n\n" +
"_You will only be asked this once._",
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: new MenuBuilder(null!).SaveAddressConfirmMenu()
);
}
else
{
// Already has preference - apply it automatically
if (session.Privacy.SaveShippingAddress == true)
{
session.SavedAddress = new SavedShippingAddress
{
Name = session.OrderFlow.ShippingName,
Address = session.OrderFlow.ShippingAddress,
City = session.OrderFlow.ShippingCity,
PostCode = session.OrderFlow.ShippingPostCode,
Country = session.OrderFlow.ShippingCountry,
SavedAt = DateTime.UtcNow
};
}
// Skip to review step
session.OrderFlow.CurrentStep = OrderFlowStep.ReviewingOrder;
@ -144,6 +173,7 @@ namespace TeleBot.Handlers
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
replyMarkup: MenuBuilder.CheckoutConfirmMenu()
);
}
break;
// Legacy steps - redirect to new single-message collection

View File

@ -25,6 +25,9 @@ namespace TeleBot.Models
public bool IsEphemeral { get; set; } = true;
public PrivacySettings Privacy { get; set; } = new();
// Saved shipping address (optional, based on user preference)
public SavedShippingAddress? SavedAddress { get; set; }
// Order flow data (temporary)
public OrderFlowData? OrderFlow { get; set; }
@ -62,6 +65,19 @@ namespace TeleBot.Models
public bool EnableDisappearingMessages { get; set; } = true;
public int DisappearingMessageTTL { get; set; } = 30; // seconds
// Address memory preference
public bool? SaveShippingAddress { get; set; } = null; // null = not asked yet, true = save, false = don't save
}
public class SavedShippingAddress
{
public string? Name { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? PostCode { get; set; }
public string? Country { get; set; }
public DateTime? SavedAt { get; set; }
}
public class OrderFlowData

View File

@ -217,6 +217,35 @@ namespace TeleBot.UI
});
}
public InlineKeyboardMarkup UseSavedAddressMenu()
{
return new InlineKeyboardMarkup(new[]
{
new[] {
InlineKeyboardButton.WithCallbackData("✅ Use Saved Address", "use_saved_address")
},
new[] {
InlineKeyboardButton.WithCallbackData("✏️ Enter New Address", "enter_new_address")
},
new[] {
InlineKeyboardButton.WithCallbackData("❌ Cancel", "cart")
}
});
}
public InlineKeyboardMarkup SaveAddressConfirmMenu()
{
return new InlineKeyboardMarkup(new[]
{
new[] {
InlineKeyboardButton.WithCallbackData("✅ Yes, Save My Address", "save_address_yes")
},
new[] {
InlineKeyboardButton.WithCallbackData("❌ No, Don't Save", "save_address_no")
}
});
}
public static InlineKeyboardMarkup PaymentMethodMenu(List<string> currencies)
{
var buttons = new List<InlineKeyboardButton[]>();