Add SMS quote approval, fix Twilio credentials, fix passkey post-login redirect
- Add 'Send Quote via SMS' button on quote details page that sends the approval link to the customer via SMS (respects NotifyBySms, handles prospects via ProspectPhone) - Reuses existing valid approval token rather than regenerating, so a previously emailed link stays valid when SMS is also sent - Fix Twilio appsettings.json placeholders (real credentials moved to gitignored appsettings.Development.json) - Fix passkey login ignoring ReturnUrl: biometric login on the login page now respects the form's ReturnUrl hidden field so QR-code and deep-link flows redirect correctly after authentication instead of always going to the dashboard Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3291,6 +3291,68 @@ public class QuotesController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the quote approval link to the customer via SMS.
|
||||
/// Reuses the existing approval token when valid; generates a new one only when none exists or it is expired.
|
||||
/// Does NOT regenerate a live token — so a previously emailed link stays valid.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> SendQuoteApprovalSms(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var quote = await _unitOfWork.Quotes.GetByIdAsync(id, false, q => q.Customer, q => q.QuoteStatus);
|
||||
if (quote == null)
|
||||
return Json(new { success = false, message = "Quote not found." });
|
||||
|
||||
// Determine recipient phone for the feedback message
|
||||
string? recipientPhone = quote.CustomerId.HasValue
|
||||
? (quote.Customer?.MobilePhone ?? quote.Customer?.Phone)
|
||||
: quote.ProspectPhone;
|
||||
|
||||
string recipientName = quote.CustomerId.HasValue && quote.Customer != null
|
||||
? (!string.IsNullOrWhiteSpace(quote.Customer.CompanyName)
|
||||
? quote.Customer.CompanyName
|
||||
: $"{quote.Customer.ContactFirstName} {quote.Customer.ContactLastName}".Trim())
|
||||
: (!string.IsNullOrWhiteSpace(quote.ProspectContactName) ? quote.ProspectContactName
|
||||
: quote.ProspectCompanyName ?? "Prospect");
|
||||
|
||||
// Ensure a valid (non-expired) approval token exists — generate only if missing or expired
|
||||
bool tokenChanged = false;
|
||||
if (string.IsNullOrEmpty(quote.ApprovalToken) ||
|
||||
(quote.ApprovalTokenExpiresAt.HasValue && quote.ApprovalTokenExpiresAt.Value < DateTime.UtcNow))
|
||||
{
|
||||
var tokenBytes = System.Security.Cryptography.RandomNumberGenerator.GetBytes(32);
|
||||
quote.ApprovalToken = Convert.ToBase64String(tokenBytes)
|
||||
.Replace('+', '-').Replace('/', '_').TrimEnd('=');
|
||||
quote.ApprovalTokenExpiresAt = DateTime.UtcNow.AddDays(
|
||||
int.TryParse(await _platformSettings.GetAsync(PlatformSettingKeys.QuoteApprovalTokenDays), out var td) ? td : 30);
|
||||
quote.ApprovalTokenUsedAt = null;
|
||||
tokenChanged = true;
|
||||
}
|
||||
|
||||
if (tokenChanged)
|
||||
{
|
||||
await _unitOfWork.Quotes.UpdateAsync(quote);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
}
|
||||
|
||||
var (success, error) = await _notificationService.NotifyQuoteSentSmsAsync(quote);
|
||||
|
||||
if (!success)
|
||||
return Json(new { success = false, message = error ?? "SMS could not be sent." });
|
||||
|
||||
var phone = string.IsNullOrWhiteSpace(recipientPhone) ? "their phone" : recipientPhone;
|
||||
return Json(new { success = true, message = $"Approval link sent to {recipientName} via SMS ({phone})." });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error sending quote approval SMS for quote {QuoteId}", id);
|
||||
return Json(new { success = false, message = "An unexpected error occurred. Please try again." });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the notification delivery history for a quote as a JSON array.
|
||||
/// Used by the "Notifications Sent" tab on the Details page to show which emails/SMS
|
||||
|
||||
Reference in New Issue
Block a user