From d1cb857aa7fa5c8ed79bf60eead667c93e3545fa Mon Sep 17 00:00:00 2001
From: jackiettran <41605212+jackiettran@users.noreply.github.com>
Date: Tue, 28 Oct 2025 22:23:41 -0400
Subject: [PATCH] payment confirmation for renter after rental request
approval, first listing celebration email, removed burstprotection for google
places autocomplete, renamed email templates
---
backend/models/Rental.js | 9 +
backend/routes/items.js | 25 +-
backend/routes/maps.js | 2 -
backend/routes/rentals.js | 143 ++++-
backend/services/emailService.js | 598 ++++++++++++++++--
backend/services/stripeService.js | 37 +-
...html => conditionCheckReminderToUser.html} | 0
...ageReportCS.html => damageReportToCS.html} | 0
...tion.html => emailVerificationToUser.html} | 0
.../firstListingCelebrationToOwner.html | 314 +++++++++
...{lateReturnCS.html => lateReturnToCS.html} | 0
.../{lostItemCS.html => lostItemToCS.html} | 0
...hanged.html => passwordChangedToUser.html} | 0
...ordReset.html => passwordResetToUser.html} | 0
...ceived.html => payoutReceivedToOwner.html} | 0
.../rentalApprovalConfirmationToOwner.html | 357 +++++++++++
...rentalCancellationConfirmationToUser.html} | 0
...rentalCancellationNotificationToUser.html} | 0
.../rentalCompletionCongratsToOwner.html | 372 +++++++++++
.../rentalCompletionThankYouToRenter.html | 314 +++++++++
...ion.html => rentalConfirmationToUser.html} | 2 +
...lined.html => rentalDeclinedToRenter.html} | 0
...=> rentalRequestConfirmationToRenter.html} | 0
...Request.html => rentalRequestToOwner.html} | 0
frontend/src/services/placesService.ts | 51 +-
25 files changed, 2171 insertions(+), 53 deletions(-)
rename backend/templates/emails/{conditionCheckReminder.html => conditionCheckReminderToUser.html} (100%)
rename backend/templates/emails/{damageReportCS.html => damageReportToCS.html} (100%)
rename backend/templates/emails/{emailVerification.html => emailVerificationToUser.html} (100%)
create mode 100644 backend/templates/emails/firstListingCelebrationToOwner.html
rename backend/templates/emails/{lateReturnCS.html => lateReturnToCS.html} (100%)
rename backend/templates/emails/{lostItemCS.html => lostItemToCS.html} (100%)
rename backend/templates/emails/{passwordChanged.html => passwordChangedToUser.html} (100%)
rename backend/templates/emails/{passwordReset.html => passwordResetToUser.html} (100%)
rename backend/templates/emails/{payoutReceived.html => payoutReceivedToOwner.html} (100%)
create mode 100644 backend/templates/emails/rentalApprovalConfirmationToOwner.html
rename backend/templates/emails/{rentalCancellationConfirmation.html => rentalCancellationConfirmationToUser.html} (100%)
rename backend/templates/emails/{rentalCancellationNotification.html => rentalCancellationNotificationToUser.html} (100%)
create mode 100644 backend/templates/emails/rentalCompletionCongratsToOwner.html
create mode 100644 backend/templates/emails/rentalCompletionThankYouToRenter.html
rename backend/templates/emails/{rentalConfirmation.html => rentalConfirmationToUser.html} (99%)
rename backend/templates/emails/{rentalDeclined.html => rentalDeclinedToRenter.html} (100%)
rename backend/templates/emails/{rentalRequestConfirmation.html => rentalRequestConfirmationToRenter.html} (100%)
rename backend/templates/emails/{rentalRequest.html => rentalRequestToOwner.html} (100%)
diff --git a/backend/models/Rental.js b/backend/models/Rental.js
index 27ebeb6..9008330 100644
--- a/backend/models/Rental.js
+++ b/backend/models/Rental.js
@@ -108,6 +108,15 @@ const Rental = sequelize.define("Rental", {
stripePaymentIntentId: {
type: DataTypes.STRING,
},
+ paymentMethodBrand: {
+ type: DataTypes.STRING,
+ },
+ paymentMethodLast4: {
+ type: DataTypes.STRING,
+ },
+ chargedAt: {
+ type: DataTypes.DATE,
+ },
deliveryMethod: {
type: DataTypes.ENUM("pickup", "delivery"),
defaultValue: "pickup",
diff --git a/backend/routes/items.js b/backend/routes/items.js
index 615dceb..3d5e259 100644
--- a/backend/routes/items.js
+++ b/backend/routes/items.js
@@ -225,16 +225,37 @@ router.post("/", authenticateToken, requireVerifiedEmail, async (req, res) => {
{
model: User,
as: "owner",
- attributes: ["id", "username", "firstName", "lastName"],
+ attributes: ["id", "username", "firstName", "lastName", "email", "stripeConnectedAccountId"],
},
],
});
+ // Check if this is the owner's first listing
+ const ownerItemCount = await Item.count({
+ where: { ownerId: req.user.id }
+ });
+
+ // If first listing, send celebration email
+ if (ownerItemCount === 1) {
+ try {
+ const emailService = require("../services/emailService");
+ await emailService.sendFirstListingCelebrationEmail(
+ itemWithOwner.owner,
+ itemWithOwner
+ );
+ console.log(`First listing celebration email sent to owner ${req.user.id}`);
+ } catch (emailError) {
+ // Log but don't fail the item creation
+ console.error('Failed to send first listing celebration email:', emailError.message);
+ }
+ }
+
const reqLogger = logger.withRequestId(req.id);
reqLogger.info("Item created", {
itemId: item.id,
ownerId: req.user.id,
- itemName: req.body.name
+ itemName: req.body.name,
+ isFirstListing: ownerItemCount === 1
});
res.status(201).json(itemWithOwner);
diff --git a/backend/routes/maps.js b/backend/routes/maps.js
index f29b381..c54b2cb 100644
--- a/backend/routes/maps.js
+++ b/backend/routes/maps.js
@@ -69,7 +69,6 @@ const handleServiceError = (error, res, req) => {
router.post(
"/places/autocomplete",
authenticateToken,
- rateLimiter.burstProtection,
rateLimiter.placesAutocomplete,
validateInput,
async (req, res) => {
@@ -150,7 +149,6 @@ router.post(
router.post(
"/geocode",
authenticateToken,
- rateLimiter.burstProtection,
rateLimiter.geocoding,
validateInput,
async (req, res) => {
diff --git a/backend/routes/rentals.js b/backend/routes/rentals.js
index 0afd412..a463a91 100644
--- a/backend/routes/rentals.js
+++ b/backend/routes/rentals.js
@@ -404,6 +404,9 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
status: "confirmed",
paymentStatus: "paid",
stripePaymentIntentId: paymentResult.paymentIntentId,
+ paymentMethodBrand: paymentResult.paymentMethod?.brand || null,
+ paymentMethodLast4: paymentResult.paymentMethod?.last4 || null,
+ chargedAt: paymentResult.chargedAt || new Date(),
});
const updatedRental = await Rental.findByPk(rental.id, {
@@ -423,7 +426,58 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
});
// Send confirmation emails
- await emailService.sendRentalConfirmationEmails(updatedRental);
+ // Send approval confirmation to owner with Stripe reminder
+ try {
+ await emailService.sendRentalApprovalConfirmationEmail(updatedRental);
+ const reqLogger = logger.withRequestId(req.id);
+ reqLogger.info("Rental approval confirmation sent to owner", {
+ rentalId: updatedRental.id,
+ ownerId: updatedRental.ownerId,
+ });
+ } catch (emailError) {
+ const reqLogger = logger.withRequestId(req.id);
+ reqLogger.error("Failed to send rental approval confirmation email to owner", {
+ error: emailError.message,
+ rentalId: updatedRental.id,
+ ownerId: updatedRental.ownerId,
+ });
+ }
+
+ // Send rental confirmation to renter with payment receipt
+ try {
+ const renter = await User.findByPk(updatedRental.renterId, {
+ attributes: ["email", "firstName"],
+ });
+ if (renter) {
+ const renterNotification = {
+ type: "rental_confirmed",
+ title: "Rental Confirmed",
+ message: `Your rental of "${updatedRental.item.name}" has been confirmed.`,
+ rentalId: updatedRental.id,
+ userId: updatedRental.renterId,
+ metadata: { rentalStart: updatedRental.startDateTime },
+ };
+ await emailService.sendRentalConfirmation(
+ renter.email,
+ renterNotification,
+ updatedRental,
+ renter.firstName,
+ true // isRenter = true to show payment receipt
+ );
+ const reqLogger = logger.withRequestId(req.id);
+ reqLogger.info("Rental confirmation sent to renter", {
+ rentalId: updatedRental.id,
+ renterId: updatedRental.renterId,
+ });
+ }
+ } catch (emailError) {
+ const reqLogger = logger.withRequestId(req.id);
+ reqLogger.error("Failed to send rental confirmation email to renter", {
+ error: emailError.message,
+ rentalId: updatedRental.id,
+ renterId: updatedRental.renterId,
+ });
+ }
res.json(updatedRental);
return;
@@ -464,7 +518,58 @@ router.put("/:id/status", authenticateToken, async (req, res) => {
});
// Send confirmation emails
- await emailService.sendRentalConfirmationEmails(updatedRental);
+ // Send approval confirmation to owner (for free rentals, no Stripe reminder shown)
+ try {
+ await emailService.sendRentalApprovalConfirmationEmail(updatedRental);
+ const reqLogger = logger.withRequestId(req.id);
+ reqLogger.info("Rental approval confirmation sent to owner", {
+ rentalId: updatedRental.id,
+ ownerId: updatedRental.ownerId,
+ });
+ } catch (emailError) {
+ const reqLogger = logger.withRequestId(req.id);
+ reqLogger.error("Failed to send rental approval confirmation email to owner", {
+ error: emailError.message,
+ rentalId: updatedRental.id,
+ ownerId: updatedRental.ownerId,
+ });
+ }
+
+ // Send rental confirmation to renter
+ try {
+ const renter = await User.findByPk(updatedRental.renterId, {
+ attributes: ["email", "firstName"],
+ });
+ if (renter) {
+ const renterNotification = {
+ type: "rental_confirmed",
+ title: "Rental Confirmed",
+ message: `Your rental of "${updatedRental.item.name}" has been confirmed.`,
+ rentalId: updatedRental.id,
+ userId: updatedRental.renterId,
+ metadata: { rentalStart: updatedRental.startDateTime },
+ };
+ await emailService.sendRentalConfirmation(
+ renter.email,
+ renterNotification,
+ updatedRental,
+ renter.firstName,
+ true // isRenter = true (for free rentals, shows "no payment required")
+ );
+ const reqLogger = logger.withRequestId(req.id);
+ reqLogger.info("Rental confirmation sent to renter", {
+ rentalId: updatedRental.id,
+ renterId: updatedRental.renterId,
+ });
+ }
+ } catch (emailError) {
+ const reqLogger = logger.withRequestId(req.id);
+ reqLogger.error("Failed to send rental confirmation email to renter", {
+ error: emailError.message,
+ rentalId: updatedRental.id,
+ renterId: updatedRental.renterId,
+ });
+ }
res.json(updatedRental);
return;
@@ -958,6 +1063,40 @@ router.post("/:id/mark-return", authenticateToken, async (req, res) => {
actualReturnDateTime: actualReturnDateTime || rental.endDateTime,
notes: notes || null,
});
+
+ // Fetch full rental details with associations for email
+ const rentalWithDetails = await Rental.findByPk(rentalId, {
+ include: [
+ { model: Item, as: "item" },
+ {
+ model: User,
+ as: "owner",
+ attributes: ["id", "username", "firstName", "lastName"],
+ },
+ {
+ model: User,
+ as: "renter",
+ attributes: ["id", "username", "firstName", "lastName"],
+ },
+ ],
+ });
+
+ // Send completion emails to both renter and owner
+ try {
+ await emailService.sendRentalCompletionEmails(rentalWithDetails);
+ const reqLogger = logger.withRequestId(req.id);
+ reqLogger.info("Rental completion emails sent", {
+ rentalId,
+ ownerId: rental.ownerId,
+ renterId: rental.renterId,
+ });
+ } catch (emailError) {
+ const reqLogger = logger.withRequestId(req.id);
+ reqLogger.error("Failed to send rental completion emails", {
+ error: emailError.message,
+ rentalId,
+ });
+ }
break;
case "damaged":
diff --git a/backend/services/emailService.js b/backend/services/emailService.js
index a14f7d5..7c10a2e 100644
--- a/backend/services/emailService.js
+++ b/backend/services/emailService.js
@@ -33,20 +33,24 @@ class EmailService {
try {
const templateFiles = [
- "conditionCheckReminder.html",
- "rentalConfirmation.html",
- "emailVerification.html",
- "passwordReset.html",
- "passwordChanged.html",
- "lateReturnCS.html",
- "damageReportCS.html",
- "lostItemCS.html",
- "rentalRequest.html",
- "rentalRequestConfirmation.html",
- "rentalCancellationConfirmation.html",
- "rentalCancellationNotification.html",
- "rentalDeclined.html",
- "payoutReceived.html",
+ "conditionCheckReminderToUser.html",
+ "rentalConfirmationToUser.html",
+ "emailVerificationToUser.html",
+ "passwordResetToUser.html",
+ "passwordChangedToUser.html",
+ "lateReturnToCS.html",
+ "damageReportToCS.html",
+ "lostItemToCS.html",
+ "rentalRequestToOwner.html",
+ "rentalRequestConfirmationToRenter.html",
+ "rentalCancellationConfirmationToUser.html",
+ "rentalCancellationNotificationToUser.html",
+ "rentalDeclinedToRenter.html",
+ "rentalApprovalConfirmationToOwner.html",
+ "rentalCompletionThankYouToRenter.html",
+ "rentalCompletionCongratsToOwner.html",
+ "payoutReceivedToOwner.html",
+ "firstListingCelebrationToOwner.html",
];
for (const templateFile of templateFiles) {
@@ -222,7 +226,7 @@ class EmailService {
`;
const templates = {
- conditionCheckReminder: baseTemplate.replace(
+ conditionCheckReminderToUser: baseTemplate.replace(
"{{content}}",
`
{{title}}
@@ -233,7 +237,7 @@ class EmailService {
`
),
- rentalConfirmation: baseTemplate.replace(
+ rentalConfirmationToUser: baseTemplate.replace(
"{{content}}",
`
Hi {{recipientName}},
@@ -245,7 +249,7 @@ class EmailService {
`
),
- emailVerification: baseTemplate.replace(
+ emailVerificationToUser: baseTemplate.replace(
"{{content}}",
`
Hi {{recipientName}},
@@ -257,7 +261,7 @@ class EmailService {
`
),
- passwordReset: baseTemplate.replace(
+ passwordResetToUser: baseTemplate.replace(
"{{content}}",
`
Hi {{recipientName}},
@@ -270,7 +274,7 @@ class EmailService {
`
),
- passwordChanged: baseTemplate.replace(
+ passwordChangedToUser: baseTemplate.replace(
"{{content}}",
`
Hi {{recipientName}},
@@ -282,7 +286,7 @@ class EmailService {
`
),
- rentalRequest: baseTemplate.replace(
+ rentalRequestToOwner: baseTemplate.replace(
"{{content}}",
`
Hi {{ownerName}},
@@ -298,7 +302,7 @@ class EmailService {
`
),
- rentalRequestConfirmation: baseTemplate.replace(
+ rentalRequestConfirmationToRenter: baseTemplate.replace(
"{{content}}",
`
Hi {{renterName}},
@@ -314,7 +318,7 @@ class EmailService {
`
),
- rentalCancellationConfirmation: baseTemplate.replace(
+ rentalCancellationConfirmationToUser: baseTemplate.replace(
"{{content}}",
`
Hi {{recipientName}},
@@ -328,7 +332,7 @@ class EmailService {
`
),
- rentalCancellationNotification: baseTemplate.replace(
+ rentalCancellationNotificationToUser: baseTemplate.replace(
"{{content}}",
`
Hi {{recipientName}},
@@ -343,7 +347,7 @@ class EmailService {
`
),
- payoutReceived: baseTemplate.replace(
+ payoutReceivedToOwner: baseTemplate.replace(
"{{content}}",
`
Hi {{ownerName}},
@@ -363,7 +367,7 @@ class EmailService {
`
),
- rentalDeclined: baseTemplate.replace(
+ rentalDeclinedToRenter: baseTemplate.replace(
"{{content}}",
`
Hi {{renterName}},
@@ -384,6 +388,60 @@ class EmailService {
If you have any questions or concerns, please don't hesitate to contact our support team.
`
),
+
+ rentalApprovalConfirmationToOwner: baseTemplate.replace(
+ "{{content}}",
+ `
+ Hi {{ownerName}},
+ You've Approved the Rental Request!
+ You've successfully approved the rental request for {{itemName}}.
+ Rental Details
+ Item: {{itemName}}
+ Renter: {{renterName}}
+ Start Date: {{startDate}}
+ End Date: {{endDate}}
+ Your Earnings: \${{payoutAmount}}
+ {{stripeSection}}
+ What's Next?
+
+ - Coordinate with the renter on pickup details
+ - Take photos of the item's condition before handoff
+ - Provide any care instructions or usage tips
+
+ View Rental Details
+ `
+ ),
+
+ rentalCompletionThankYouToRenter: baseTemplate.replace(
+ "{{content}}",
+ `
+ Hi {{renterName}},
+ Thank You for Returning On Time!
+ You've successfully returned {{itemName}} on time. On-time returns like yours help build trust in the RentAll community!
+ Rental Summary
+ Item: {{itemName}}
+ Rental Period: {{startDate}} to {{endDate}}
+ Returned On: {{returnedDate}}
+ {{reviewSection}}
+ Browse Available Items
+ `
+ ),
+
+ rentalCompletionCongratsToOwner: baseTemplate.replace(
+ "{{content}}",
+ `
+ Hi {{ownerName}},
+ Congratulations on Completing a Rental!
+ {{itemName}} has been successfully returned on time. Great job!
+ Rental Summary
+ Item: {{itemName}}
+ Renter: {{renterName}}
+ Rental Period: {{startDate}} to {{endDate}}
+ {{earningsSection}}
+ {{stripeSection}}
+ View My Listings
+ `
+ ),
};
return (
@@ -409,7 +467,7 @@ class EmailService {
};
const htmlContent = this.renderTemplate(
- "conditionCheckReminder",
+ "conditionCheckReminderToUser",
variables
);
@@ -424,7 +482,8 @@ class EmailService {
userEmail,
notification,
rental,
- recipientName = null
+ recipientName = null,
+ isRenter = false
) {
const itemName = rental?.item?.name || "Unknown Item";
@@ -439,9 +498,77 @@ class EmailService {
endDate: rental?.endDateTime
? new Date(rental.endDateTime).toLocaleDateString()
: "Not specified",
+ isRenter: isRenter,
};
- const htmlContent = this.renderTemplate("rentalConfirmation", variables);
+ // Add payment information if this is for the renter and rental has payment info
+ let paymentSection = "";
+ if (isRenter) {
+ const totalAmount = parseFloat(rental.totalAmount) || 0;
+ const isPaidRental = totalAmount > 0 && rental.paymentStatus === 'paid';
+
+ if (isPaidRental) {
+ // Format payment method display
+ let paymentMethodDisplay = "Payment method on file";
+ if (rental.paymentMethodBrand && rental.paymentMethodLast4) {
+ const brandCapitalized = rental.paymentMethodBrand.charAt(0).toUpperCase() +
+ rental.paymentMethodBrand.slice(1);
+ paymentMethodDisplay = `${brandCapitalized} ending in ${rental.paymentMethodLast4}`;
+ }
+
+ const chargedAtFormatted = rental.chargedAt
+ ? new Date(rental.chargedAt).toLocaleString("en-US", {
+ dateStyle: "medium",
+ timeStyle: "short"
+ })
+ : new Date().toLocaleString("en-US", {
+ dateStyle: "medium",
+ timeStyle: "short"
+ });
+
+ // Build payment receipt section HTML
+ paymentSection = `
+ Payment Receipt
+
+
💳
+
Payment Successful
+
Your payment has been processed. This email serves as your receipt.
+
+
+
+ | Amount Charged |
+ $${totalAmount.toFixed(2)} |
+
+
+ | Payment Method |
+ ${paymentMethodDisplay} |
+
+
+ | Transaction ID |
+ ${rental.stripePaymentIntentId || "N/A"} |
+
+
+ | Transaction Date |
+ ${chargedAtFormatted} |
+
+
+
+ Note: Keep this email for your records. You can use the transaction ID above if you need to contact support about this payment.
+
+ `;
+ } else if (totalAmount === 0) {
+ // Free rental message
+ paymentSection = `
+
+
No Payment Required: This is a free rental.
+
+ `;
+ }
+ }
+
+ variables.paymentSection = paymentSection;
+
+ const htmlContent = this.renderTemplate("rentalConfirmationToUser", variables);
// Use clear, transactional subject line with item name
const subject = `Rental Confirmation - ${itemName}`;
@@ -458,7 +585,7 @@ class EmailService {
verificationUrl: verificationUrl,
};
- const htmlContent = this.renderTemplate("emailVerification", variables);
+ const htmlContent = this.renderTemplate("emailVerificationToUser", variables);
return await this.sendEmail(
user.email,
@@ -476,7 +603,7 @@ class EmailService {
resetUrl: resetUrl,
};
- const htmlContent = this.renderTemplate("passwordReset", variables);
+ const htmlContent = this.renderTemplate("passwordResetToUser", variables);
return await this.sendEmail(
user.email,
@@ -497,7 +624,7 @@ class EmailService {
timestamp: timestamp,
};
- const htmlContent = this.renderTemplate("passwordChanged", variables);
+ const htmlContent = this.renderTemplate("passwordChangedToUser", variables);
return await this.sendEmail(
user.email,
@@ -554,7 +681,7 @@ class EmailService {
approveUrl: approveUrl,
};
- const htmlContent = this.renderTemplate("rentalRequest", variables);
+ const htmlContent = this.renderTemplate("rentalRequestToOwner", variables);
return await this.sendEmail(
owner.email,
@@ -608,7 +735,7 @@ class EmailService {
};
const htmlContent = this.renderTemplate(
- "rentalRequestConfirmation",
+ "rentalRequestConfirmationToRenter",
variables
);
@@ -678,7 +805,7 @@ class EmailService {
};
const htmlContent = this.renderTemplate(
- "rentalDeclined",
+ "rentalDeclinedToRenter",
variables
);
@@ -730,7 +857,7 @@ class EmailService {
earningsDashboardUrl: earningsDashboardUrl,
};
- const htmlContent = this.renderTemplate("payoutReceived", variables);
+ const htmlContent = this.renderTemplate("payoutReceivedToOwner", variables);
return await this.sendEmail(
owner.email,
@@ -874,7 +1001,7 @@ class EmailService {
};
const confirmationHtml = this.renderTemplate(
- "rentalCancellationConfirmation",
+ "rentalCancellationConfirmationToUser",
confirmationVariables
);
@@ -910,7 +1037,7 @@ class EmailService {
};
const notificationHtml = this.renderTemplate(
- "rentalCancellationNotification",
+ "rentalCancellationNotificationToUser",
notificationVariables
);
@@ -965,7 +1092,7 @@ class EmailService {
await this.sendTemplateEmail(
process.env.CUSTOMER_SUPPORT_EMAIL,
"Late Return Detected - Action Required",
- "lateReturnCS",
+ "lateReturnToCS",
{
rentalId: rental.id,
itemName: rental.item.name,
@@ -1027,7 +1154,7 @@ class EmailService {
await this.sendTemplateEmail(
process.env.CUSTOMER_SUPPORT_EMAIL,
"Damage Report Filed - Action Required",
- "damageReportCS",
+ "damageReportToCS",
{
rentalId: rental.id,
itemName: rental.item.name,
@@ -1086,7 +1213,7 @@ class EmailService {
await this.sendTemplateEmail(
process.env.CUSTOMER_SUPPORT_EMAIL,
"Lost Item Claim Filed - Action Required",
- "lostItemCS",
+ "lostItemToCS",
{
rentalId: rental.id,
itemName: rental.item.name,
@@ -1153,7 +1280,8 @@ class EmailService {
owner.email,
ownerNotification,
rental,
- owner.firstName
+ owner.firstName,
+ false // isRenter = false for owner
);
if (ownerResult.success) {
console.log(
@@ -1181,7 +1309,8 @@ class EmailService {
renter.email,
renterNotification,
rental,
- renter.firstName
+ renter.firstName,
+ true // isRenter = true for renter (enables payment receipt)
);
if (renterResult.success) {
console.log(
@@ -1210,6 +1339,391 @@ class EmailService {
return results;
}
+
+ async sendFirstListingCelebrationEmail(owner, item) {
+ const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
+
+ const variables = {
+ ownerName: owner.firstName || "there",
+ itemName: item.name,
+ itemId: item.id,
+ viewItemUrl: `${frontendUrl}/items/${item.id}`,
+ };
+
+ const htmlContent = this.renderTemplate(
+ "firstListingCelebrationToOwner",
+ variables
+ );
+
+ const subject = `🎉 Congratulations! Your first item is live on RentAll`;
+
+ return await this.sendEmail(owner.email, subject, htmlContent);
+ }
+
+ async sendRentalApprovalConfirmationEmail(rental) {
+ const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
+
+ // Fetch owner details
+ const owner = await User.findByPk(rental.ownerId, {
+ attributes: ["email", "firstName", "lastName", "stripeConnectedAccountId"],
+ });
+
+ // Fetch renter details
+ const renter = await User.findByPk(rental.renterId, {
+ attributes: ["firstName", "lastName"],
+ });
+
+ if (!owner || !renter) {
+ console.error(
+ "Owner or renter not found for rental approval confirmation email"
+ );
+ return { success: false, error: "User not found" };
+ }
+
+ // Determine if Stripe setup is needed
+ const hasStripeAccount = !!owner.stripeConnectedAccountId;
+ const totalAmount = parseFloat(rental.totalAmount) || 0;
+ const payoutAmount = parseFloat(rental.payoutAmount) || 0;
+ const platformFee = parseFloat(rental.platformFee) || 0;
+
+ // Build payment message
+ const isPaidRental = totalAmount > 0;
+ let paymentMessage = "";
+ if (isPaidRental) {
+ paymentMessage = "their payment has been processed successfully.";
+ } else {
+ paymentMessage = "this is a free rental (no payment required).";
+ }
+
+ // Build earnings section (only for paid rentals)
+ let earningsSection = "";
+ if (isPaidRental) {
+ earningsSection = `
+ Your Earnings
+
+
+ | Total Rental Amount |
+ \$${totalAmount.toFixed(2)} |
+
+
+ | Platform Fee (20%) |
+ -\$${platformFee.toFixed(2)} |
+
+
+ | Your Payout |
+ \$${payoutAmount.toFixed(2)} |
+
+
+ `;
+ }
+
+ // Build conditional Stripe section based on Stripe status
+ let stripeSection = "";
+ if (!hasStripeAccount && isPaidRental) {
+ // Only show Stripe setup reminder for paid rentals
+ stripeSection = `
+
+
⚠️ Action Required: Set Up Your Earnings Account
+
To receive your payout of \$${payoutAmount.toFixed(2)} when this rental completes, you need to set up your earnings account.
+
+ Set Up Earnings to Get Paid
+
+
Why set up now?
+
+ - Automatic payouts when rentals complete
+ - Secure transfers directly to your bank account
+ - Track all earnings in one dashboard
+ - Fast deposits (typically 2-3 business days)
+
+
Setup only takes about 5 minutes and you only need to do it once.
+
+
+ Set Up Earnings Account Now
+
+
+ Important: Without earnings setup, you won't receive payouts automatically when rentals complete.
+
+ `;
+ } else if (hasStripeAccount && isPaidRental) {
+ stripeSection = `
+
+
✓ Earnings Account Active
+
Your earnings account is set up. You'll automatically receive \$${payoutAmount.toFixed(2)} when this rental completes.
+
View your earnings dashboard →
+
+ `;
+ }
+
+ // Format delivery method for display
+ const deliveryMethodDisplay = rental.deliveryMethod === "delivery" ? "Delivery" : "Pickup";
+
+ const variables = {
+ ownerName: owner.firstName || "there",
+ itemName: rental.item?.name || "your item",
+ renterName: `${renter.firstName} ${renter.lastName}`.trim() || "The renter",
+ startDate: rental.startDateTime
+ ? new Date(rental.startDateTime).toLocaleString("en-US", {
+ dateStyle: "medium",
+ timeStyle: "short",
+ })
+ : "Not specified",
+ endDate: rental.endDateTime
+ ? new Date(rental.endDateTime).toLocaleString("en-US", {
+ dateStyle: "medium",
+ timeStyle: "short",
+ })
+ : "Not specified",
+ deliveryMethod: deliveryMethodDisplay,
+ paymentMessage: paymentMessage,
+ earningsSection: earningsSection,
+ stripeSection: stripeSection,
+ rentalDetailsUrl: `${frontendUrl}/my-listings?rentalId=${rental.id}`,
+ };
+
+ const htmlContent = this.renderTemplate(
+ "rentalApprovalConfirmationToOwner",
+ variables
+ );
+
+ const subject = `Rental Approved - ${rental.item?.name || "Your Item"}`;
+
+ return await this.sendEmail(owner.email, subject, htmlContent);
+ }
+
+ async sendRentalCompletionEmails(rental) {
+ const frontendUrl = process.env.FRONTEND_URL || "http://localhost:3000";
+ const results = {
+ renterEmailSent: false,
+ ownerEmailSent: false,
+ };
+
+ try {
+ // Fetch owner details with Stripe info
+ const owner = await User.findByPk(rental.ownerId, {
+ attributes: ["email", "firstName", "lastName", "stripeConnectedAccountId"],
+ });
+
+ // Fetch renter details
+ const renter = await User.findByPk(rental.renterId, {
+ attributes: ["email", "firstName", "lastName"],
+ });
+
+ if (!owner || !renter) {
+ console.error(
+ "Owner or renter not found for rental completion emails"
+ );
+ return { success: false, error: "User not found" };
+ }
+
+ // Format dates
+ const startDate = rental.startDateTime
+ ? new Date(rental.startDateTime).toLocaleString("en-US", {
+ dateStyle: "medium",
+ timeStyle: "short",
+ })
+ : "Not specified";
+ const endDate = rental.endDateTime
+ ? new Date(rental.endDateTime).toLocaleString("en-US", {
+ dateStyle: "medium",
+ timeStyle: "short",
+ })
+ : "Not specified";
+ const returnedDate = rental.actualReturnDateTime
+ ? new Date(rental.actualReturnDateTime).toLocaleString("en-US", {
+ dateStyle: "medium",
+ timeStyle: "short",
+ })
+ : endDate;
+
+ // Check if renter has already submitted a review
+ const hasReviewed = !!rental.itemReviewSubmittedAt;
+
+ // Build review section for renter email
+ let reviewSection = "";
+ if (!hasReviewed) {
+ reviewSection = `
+ Share Your Experience
+
+
Help the community by leaving a review!
+
Your feedback helps other renters make informed decisions and supports quality listings on RentAll.
+
+ - How was the item's condition?
+ - Was the owner responsive and helpful?
+ - Would you rent this item again?
+
+
+
+ Leave a Review
+
+ `;
+ } else {
+ reviewSection = `
+
+
✓ Thank You for Your Review!
+
Your feedback has been submitted and helps strengthen the RentAll community.
+
+ `;
+ }
+
+ // Send email to renter
+ try {
+ const renterVariables = {
+ renterName: renter.firstName || "there",
+ itemName: rental.item?.name || "the item",
+ ownerName: owner.firstName || "the owner",
+ startDate: startDate,
+ endDate: endDate,
+ returnedDate: returnedDate,
+ reviewSection: reviewSection,
+ browseItemsUrl: `${frontendUrl}/`,
+ };
+
+ const renterHtmlContent = this.renderTemplate(
+ "rentalCompletionThankYouToRenter",
+ renterVariables
+ );
+
+ const renterResult = await this.sendEmail(
+ renter.email,
+ `Thank You for Returning "${rental.item?.name || "Item"}" On Time!`,
+ renterHtmlContent
+ );
+
+ if (renterResult.success) {
+ console.log(
+ `Rental completion thank you email sent to renter: ${renter.email}`
+ );
+ results.renterEmailSent = true;
+ } else {
+ console.error(
+ `Failed to send rental completion email to renter (${renter.email}):`,
+ renterResult.error
+ );
+ }
+ } catch (emailError) {
+ console.error(
+ `Failed to send rental completion email to renter (${renter.email}):`,
+ emailError.message
+ );
+ }
+
+ // Prepare owner email
+ const hasStripeAccount = !!owner.stripeConnectedAccountId;
+ const totalAmount = parseFloat(rental.totalAmount) || 0;
+ const payoutAmount = parseFloat(rental.payoutAmount) || 0;
+ const platformFee = parseFloat(rental.platformFee) || 0;
+ const isPaidRental = totalAmount > 0;
+
+ // Build earnings section for owner (only for paid rentals)
+ let earningsSection = "";
+ if (isPaidRental) {
+ earningsSection = `
+ Your Earnings
+
+
+ | Total Rental Amount |
+ \$${totalAmount.toFixed(2)} |
+
+
+ | Platform Fee (20%) |
+ -\$${platformFee.toFixed(2)} |
+
+
+ | Your Payout |
+ \$${payoutAmount.toFixed(2)} |
+
+
+
+ Your earnings will be automatically transferred to your account when the rental period ends and any dispute windows close.
+
+ `;
+ }
+
+ // Build Stripe section for owner
+ let stripeSection = "";
+ if (!hasStripeAccount && isPaidRental) {
+ // Show Stripe setup reminder for paid rentals
+ stripeSection = `
+
+
⚠️ Action Required: Set Up Your Earnings Account
+
To receive your payout of \$${payoutAmount.toFixed(2)}, you need to set up your earnings account.
+
+ Set Up Earnings to Get Paid
+
+
Why set up now?
+
+ - Automatic payouts when the rental period ends
+ - Secure transfers directly to your bank account
+ - Track all earnings in one dashboard
+ - Fast deposits (typically 2-3 business days)
+
+
Setup only takes about 5 minutes and you only need to do it once.
+
+
+ Set Up Earnings Account Now
+
+
+ Important: Without earnings setup, you won't receive payouts automatically.
+
+ `;
+ } else if (hasStripeAccount && isPaidRental) {
+ stripeSection = `
+
+
✓ Earnings Account Active
+
Your earnings account is set up. You'll automatically receive \$${payoutAmount.toFixed(2)} when the rental period ends.
+
View your earnings dashboard →
+
+ `;
+ }
+
+ // Send email to owner
+ try {
+ const ownerVariables = {
+ ownerName: owner.firstName || "there",
+ itemName: rental.item?.name || "your item",
+ renterName: `${renter.firstName} ${renter.lastName}`.trim() || "The renter",
+ startDate: startDate,
+ endDate: endDate,
+ returnedDate: returnedDate,
+ earningsSection: earningsSection,
+ stripeSection: stripeSection,
+ myListingsUrl: `${frontendUrl}/my-listings`,
+ };
+
+ const ownerHtmlContent = this.renderTemplate(
+ "rentalCompletionCongratsToOwner",
+ ownerVariables
+ );
+
+ const ownerResult = await this.sendEmail(
+ owner.email,
+ `Rental Complete - ${rental.item?.name || "Your Item"}`,
+ ownerHtmlContent
+ );
+
+ if (ownerResult.success) {
+ console.log(
+ `Rental completion congratulations email sent to owner: ${owner.email}`
+ );
+ results.ownerEmailSent = true;
+ } else {
+ console.error(
+ `Failed to send rental completion email to owner (${owner.email}):`,
+ ownerResult.error
+ );
+ }
+ } catch (emailError) {
+ console.error(
+ `Failed to send rental completion email to owner (${owner.email}):`,
+ emailError.message
+ );
+ }
+ } catch (error) {
+ console.error("Error sending rental completion emails:", error);
+ }
+
+ return results;
+ }
}
module.exports = new EmailService();
diff --git a/backend/services/stripeService.js b/backend/services/stripeService.js
index b68419f..9b53758 100644
--- a/backend/services/stripeService.js
+++ b/backend/services/stripeService.js
@@ -123,14 +123,48 @@ class StripeService {
payment_method: paymentMethodId,
customer: customerId, // Include customer ID
confirm: true, // Automatically confirm the payment
+ off_session: true, // Indicate this is an off-session payment
return_url: `${process.env.FRONTEND_URL || 'http://localhost:3000'}/payment-complete`,
metadata,
+ expand: ['charges.data.payment_method_details'], // Expand to get payment method details
});
+ // Extract payment method details from charges
+ const charge = paymentIntent.charges?.data?.[0];
+ const paymentMethodDetails = charge?.payment_method_details;
+
+ // Build payment method info object
+ let paymentMethod = null;
+ if (paymentMethodDetails) {
+ const type = paymentMethodDetails.type;
+ if (type === 'card') {
+ paymentMethod = {
+ type: 'card',
+ brand: paymentMethodDetails.card?.brand || 'card',
+ last4: paymentMethodDetails.card?.last4 || '****',
+ };
+ } else if (type === 'us_bank_account') {
+ paymentMethod = {
+ type: 'bank',
+ brand: 'bank_account',
+ last4: paymentMethodDetails.us_bank_account?.last4 || '****',
+ };
+ } else {
+ paymentMethod = {
+ type: type || 'unknown',
+ brand: type || 'payment',
+ last4: null,
+ };
+ }
+ }
+
return {
paymentIntentId: paymentIntent.id,
status: paymentIntent.status,
clientSecret: paymentIntent.client_secret,
+ paymentMethod: paymentMethod,
+ chargedAt: new Date(paymentIntent.created * 1000), // Convert Unix timestamp to Date
+ amountCharged: amount, // Original amount in dollars
};
} catch (error) {
console.error("Error charging payment method:", error);
@@ -162,9 +196,6 @@ class StripeService {
mode: 'setup',
ui_mode: 'embedded',
redirect_on_completion: 'never',
- setup_intent_data: {
- usage: 'off_session'
- },
metadata: {
type: 'payment_method_setup',
...metadata
diff --git a/backend/templates/emails/conditionCheckReminder.html b/backend/templates/emails/conditionCheckReminderToUser.html
similarity index 100%
rename from backend/templates/emails/conditionCheckReminder.html
rename to backend/templates/emails/conditionCheckReminderToUser.html
diff --git a/backend/templates/emails/damageReportCS.html b/backend/templates/emails/damageReportToCS.html
similarity index 100%
rename from backend/templates/emails/damageReportCS.html
rename to backend/templates/emails/damageReportToCS.html
diff --git a/backend/templates/emails/emailVerification.html b/backend/templates/emails/emailVerificationToUser.html
similarity index 100%
rename from backend/templates/emails/emailVerification.html
rename to backend/templates/emails/emailVerificationToUser.html
diff --git a/backend/templates/emails/firstListingCelebrationToOwner.html b/backend/templates/emails/firstListingCelebrationToOwner.html
new file mode 100644
index 0000000..6ed18ca
--- /dev/null
+++ b/backend/templates/emails/firstListingCelebrationToOwner.html
@@ -0,0 +1,314 @@
+
+
+
+
+
+
+ Your First Listing is Live!
+
+
+
+
+
+
+
+
Hi {{ownerName}},
+
+
Congratulations! You're Now a RentAll Host!
+
+
Your first item is officially live and ready to be rented. This is an exciting milestone!
+
+
+
+
+
What happens next?
+
+ - Your listing is now searchable by renters
+ - You'll receive email notifications for rental requests
+ - You can approve or decline requests based on your availability
+ - Payments are processed securely through Stripe
+
+
+
+
Tips for Success
+
+ - Respond quickly: Fast responses lead to more bookings
+ - Keep photos updated: Great photos attract more renters
+ - Be clear about condition: Take photos at pickup and return
+ - Communicate well: Clear communication = happy renters
+ - Maintain availability: Keep your calendar up to date
+
+
+
+
🌟 Pro Tip: Hosts who respond within 1 hour get 3x more bookings!
+
+
+
We're excited to have you as part of the RentAll community. If you have any questions, our support team is here to help.
+
+
Happy hosting!
+ The RentAll Team
+
+
+
+
+
+
diff --git a/backend/templates/emails/lateReturnCS.html b/backend/templates/emails/lateReturnToCS.html
similarity index 100%
rename from backend/templates/emails/lateReturnCS.html
rename to backend/templates/emails/lateReturnToCS.html
diff --git a/backend/templates/emails/lostItemCS.html b/backend/templates/emails/lostItemToCS.html
similarity index 100%
rename from backend/templates/emails/lostItemCS.html
rename to backend/templates/emails/lostItemToCS.html
diff --git a/backend/templates/emails/passwordChanged.html b/backend/templates/emails/passwordChangedToUser.html
similarity index 100%
rename from backend/templates/emails/passwordChanged.html
rename to backend/templates/emails/passwordChangedToUser.html
diff --git a/backend/templates/emails/passwordReset.html b/backend/templates/emails/passwordResetToUser.html
similarity index 100%
rename from backend/templates/emails/passwordReset.html
rename to backend/templates/emails/passwordResetToUser.html
diff --git a/backend/templates/emails/payoutReceived.html b/backend/templates/emails/payoutReceivedToOwner.html
similarity index 100%
rename from backend/templates/emails/payoutReceived.html
rename to backend/templates/emails/payoutReceivedToOwner.html
diff --git a/backend/templates/emails/rentalApprovalConfirmationToOwner.html b/backend/templates/emails/rentalApprovalConfirmationToOwner.html
new file mode 100644
index 0000000..ecca076
--- /dev/null
+++ b/backend/templates/emails/rentalApprovalConfirmationToOwner.html
@@ -0,0 +1,357 @@
+
+
+
+
+
+
+ Rental Request Approved
+
+
+
+
+
+
+
+
Hi {{ownerName}},
+
+
You've Approved the Rental Request!
+
+
+
✓
+
Great news! You've successfully approved the rental request for {{itemName}}.
+
+
+
The renter has been notified and {{paymentMessage}}
+
+
Rental Details
+
+
+ | Item |
+ {{itemName}} |
+
+
+ | Renter |
+ {{renterName}} |
+
+
+ | Start Date |
+ {{startDate}} |
+
+
+ | End Date |
+ {{endDate}} |
+
+
+ | Delivery Method |
+ {{deliveryMethod}} |
+
+
+
+ {{earningsSection}}
+
+ {{stripeSection}}
+
+
What's Next?
+
+ - Before pickup: Coordinate with the renter on exact pickup time and location
+ - Take photos: Document the item's condition before handoff
+ - Share instructions: Provide any care instructions or usage tips
+ - Stay in touch: Be available to answer questions during the rental
+ - At return: Inspect the item and confirm its condition
+
+
+
+ View Rental Details
+
+
+
Thank you for being part of the RentAll community!
+
+
+
+
+
+
diff --git a/backend/templates/emails/rentalCancellationConfirmation.html b/backend/templates/emails/rentalCancellationConfirmationToUser.html
similarity index 100%
rename from backend/templates/emails/rentalCancellationConfirmation.html
rename to backend/templates/emails/rentalCancellationConfirmationToUser.html
diff --git a/backend/templates/emails/rentalCancellationNotification.html b/backend/templates/emails/rentalCancellationNotificationToUser.html
similarity index 100%
rename from backend/templates/emails/rentalCancellationNotification.html
rename to backend/templates/emails/rentalCancellationNotificationToUser.html
diff --git a/backend/templates/emails/rentalCompletionCongratsToOwner.html b/backend/templates/emails/rentalCompletionCongratsToOwner.html
new file mode 100644
index 0000000..99fffbc
--- /dev/null
+++ b/backend/templates/emails/rentalCompletionCongratsToOwner.html
@@ -0,0 +1,372 @@
+
+
+
+
+
+
+ Rental Complete - Congratulations!
+
+
+
+
+
+
+
+
Hi {{ownerName}},
+
+
Congratulations on Completing a Rental!
+
+
+
✓
+
Rental Complete: {{itemName}} has been successfully returned on time.
+
Great job maintaining your item and providing excellent service to the renter!
+
+
+
Rental Summary
+
+
+ | Item |
+ {{itemName}} |
+
+
+ | Renter |
+ {{renterName}} |
+
+
+ | Rental Period |
+ {{startDate}} to {{endDate}} |
+
+
+ | Returned On |
+ {{returnedDate}} |
+
+
+
+ {{earningsSection}}
+
+ {{stripeSection}}
+
+
Tips for Future Rentals
+
+ - Maintain your items: Keep them clean and in good condition
+ - Update availability: Keep your calendar current
+ - Respond quickly: Fast responses lead to more bookings
+ - Set competitive pricing: Browse similar items to optimize your rates
+ - Build your reputation: Consistent quality earns great reviews
+
+
+
List More Items
+
Have more items sitting idle? Turn them into income! List camping gear, tools, party supplies, sports equipment, or anything else that others might need.
+
+
+ View My Listings
+
+
+
Thank you for being an excellent host on RentAll!
+
+
+
+
+
+
diff --git a/backend/templates/emails/rentalCompletionThankYouToRenter.html b/backend/templates/emails/rentalCompletionThankYouToRenter.html
new file mode 100644
index 0000000..5f02d59
--- /dev/null
+++ b/backend/templates/emails/rentalCompletionThankYouToRenter.html
@@ -0,0 +1,314 @@
+
+
+
+
+
+
+ Thank You for Returning On Time!
+
+
+
+
+
+
+
+
Hi {{renterName}},
+
+
Thank You for Returning On Time!
+
+
+
✓
+
Rental Complete: You've successfully returned {{itemName}} on time.
+
On-time returns like yours help build trust in the RentAll community. Thank you!
+
+
+
Rental Summary
+
+
+ | Item |
+ {{itemName}} |
+
+
+ | Owner |
+ {{ownerName}} |
+
+
+ | Rental Period |
+ {{startDate}} to {{endDate}} |
+
+
+ | Returned On |
+ {{returnedDate}} |
+
+
+
+ {{reviewSection}}
+
+
Keep Renting!
+
Explore thousands of items available for rent in your area. From tools to camping gear, party supplies to photography equipment - find what you need, when you need it.
+
+
+ Browse Available Items
+
+
+
Thank you for being a valued member of the RentAll community!
+
+
+
+
+
+
diff --git a/backend/templates/emails/rentalConfirmation.html b/backend/templates/emails/rentalConfirmationToUser.html
similarity index 99%
rename from backend/templates/emails/rentalConfirmation.html
rename to backend/templates/emails/rentalConfirmationToUser.html
index 70ce7b6..5e4c062 100644
--- a/backend/templates/emails/rentalConfirmation.html
+++ b/backend/templates/emails/rentalConfirmationToUser.html
@@ -251,6 +251,8 @@
+ {{paymentSection}}
+
What's next?
- Before pickup: You'll receive a reminder to take condition photos
diff --git a/backend/templates/emails/rentalDeclined.html b/backend/templates/emails/rentalDeclinedToRenter.html
similarity index 100%
rename from backend/templates/emails/rentalDeclined.html
rename to backend/templates/emails/rentalDeclinedToRenter.html
diff --git a/backend/templates/emails/rentalRequestConfirmation.html b/backend/templates/emails/rentalRequestConfirmationToRenter.html
similarity index 100%
rename from backend/templates/emails/rentalRequestConfirmation.html
rename to backend/templates/emails/rentalRequestConfirmationToRenter.html
diff --git a/backend/templates/emails/rentalRequest.html b/backend/templates/emails/rentalRequestToOwner.html
similarity index 100%
rename from backend/templates/emails/rentalRequest.html
rename to backend/templates/emails/rentalRequestToOwner.html
diff --git a/frontend/src/services/placesService.ts b/frontend/src/services/placesService.ts
index 3b26c52..d54d26f 100644
--- a/frontend/src/services/placesService.ts
+++ b/frontend/src/services/placesService.ts
@@ -29,6 +29,8 @@ export interface AutocompletePrediction {
class PlacesService {
private sessionToken: string | null = null;
+ private debounceTimer: NodeJS.Timeout | null = null;
+ private abortController: AbortController | null = null;
/**
* Generate a new session token for cost optimization
@@ -41,7 +43,7 @@ class PlacesService {
}
/**
- * Get autocomplete predictions for a query
+ * Get autocomplete predictions for a query (debounced)
*/
async getAutocompletePredictions(
input: string,
@@ -55,14 +57,54 @@ class PlacesService {
return [];
}
+ // Clear existing debounce timer
+ if (this.debounceTimer) {
+ clearTimeout(this.debounceTimer);
+ }
+
+ // Cancel any pending request
+ if (this.abortController) {
+ this.abortController.abort();
+ }
+
+ // Return a promise that resolves after debounce delay
+ return new Promise((resolve, reject) => {
+ this.debounceTimer = setTimeout(async () => {
+ try {
+ const predictions = await this.makeAutocompleteRequest(
+ input.trim(),
+ options
+ );
+ resolve(predictions);
+ } catch (error) {
+ reject(error);
+ }
+ }, 300); // 300ms debounce delay
+ });
+ }
+
+ /**
+ * Make the actual autocomplete API request
+ */
+ private async makeAutocompleteRequest(
+ input: string,
+ options?: {
+ types?: string[];
+ componentRestrictions?: { country: string };
+ bounds?: any;
+ }
+ ): Promise {
// Generate new session token if not exists
if (!this.sessionToken) {
this.sessionToken = this.generateSessionToken();
}
+ // Create new abort controller for this request
+ this.abortController = new AbortController();
+
try {
const response = await mapsAPI.placesAutocomplete({
- input: input.trim(),
+ input: input,
types: options?.types || ["address"],
componentRestrictions: options?.componentRestrictions,
sessionToken: this.sessionToken || undefined,
@@ -77,6 +119,11 @@ class PlacesService {
return [];
} catch (error: any) {
+ // Don't throw error if request was aborted (user is still typing)
+ if (error.name === "AbortError" || error.code === "ERR_CANCELED") {
+ return [];
+ }
+
console.error("Error fetching place predictions:", error.message);
if (error.response?.status === 429) {
throw new Error("Too many requests. Please slow down.");