diff --git a/app/api/auth/google/callback/route.ts b/app/api/auth/google/callback/route.ts index 479f46e..b0e7ca8 100644 --- a/app/api/auth/google/callback/route.ts +++ b/app/api/auth/google/callback/route.ts @@ -5,6 +5,7 @@ import { parseOAuthState, } from '@/lib/google-auth' import { createGoogleCalendarService, GoogleCalendarTokens } from '@/lib/google-calendar' +import { EMAIL_ADDRESSES } from '@/lib/config/email-addresses' /** * API Route: /api/auth/google/callback @@ -223,7 +224,7 @@ async function ensureUserRecord(userId: string) { .eq('id', userId) .single() - const userEmail = authUser?.email || `user-${userId}@localloop.app` + const userEmail = authUser?.email || EMAIL_ADDRESSES.generateUserEmail(userId) const userName = authUser?.raw_user_meta_data?.full_name || authUser?.raw_user_meta_data?.name || 'User' console.log(`[DEBUG] Retrieved user data: email=${userEmail}, name=${userName}`) diff --git a/app/api/events/cancellation/route.ts b/app/api/events/cancellation/route.ts index 73a7d85..1514da5 100644 --- a/app/api/events/cancellation/route.ts +++ b/app/api/events/cancellation/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' import { createServerSupabaseClient } from '@/lib/supabase-server' import { sendEventCancellationEmail } from '@/lib/email-service' +import { EMAIL_ADDRESSES } from '@/lib/config/email-addresses' import { z } from 'zod' // Event cancellation request schema @@ -207,7 +208,7 @@ export async function POST(request: NextRequest) { eventLocation: event.location, eventAddress: event.location_details || event.location, organizerName: organizerData?.display_name || 'Event Organizer', - organizerEmail: organizerData?.email || 'organizer@localloop.app', + organizerEmail: organizerData?.email || EMAIL_ADDRESSES.ORGANIZER, cancellationReason: cancellation_reason, refundAmount: 0, // Assuming refundAmount is not provided in the attendees refundTimeframe: refund_timeframe, diff --git a/app/api/events/reminders/route.ts b/app/api/events/reminders/route.ts index b4d4099..aac3bd5 100644 --- a/app/api/events/reminders/route.ts +++ b/app/api/events/reminders/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' import { createServerSupabaseClient } from '@/lib/supabase-server' import { sendEventReminderEmail } from '@/lib/email-service' +import { EMAIL_ADDRESSES } from '@/lib/config/email-addresses' import { z } from 'zod' // Event reminder request schema @@ -241,7 +242,7 @@ export async function POST(request: NextRequest) { eventLocation: event.location, eventAddress: event.location_details || event.location, organizerName: organizerData?.display_name || 'Event Organizer', - organizerEmail: organizerData?.email || 'organizer@localloop.app', + organizerEmail: organizerData?.email || EMAIL_ADDRESSES.ORGANIZER, rsvpId: attendee.rsvp_id, isTicketHolder: attendee.type === 'ticket', ticketCount: attendee.ticket_count, diff --git a/app/api/rsvps/[id]/route.ts b/app/api/rsvps/[id]/route.ts index 3058b7f..0df75aa 100644 --- a/app/api/rsvps/[id]/route.ts +++ b/app/api/rsvps/[id]/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' import { createServerSupabaseClient } from '@/lib/supabase-server' import { sendRSVPCancellationEmail } from '@/lib/email-service' +import { EMAIL_ADDRESSES } from '@/lib/config/email-addresses' import { z } from 'zod' // RSVP update schema @@ -173,8 +174,8 @@ export async function PATCH( : existingRsvp.guest_name || 'Guest'; const userEmail = existingRsvp.user_id - ? user?.email || 'unknown@email.com' - : existingRsvp.guest_email || 'unknown@email.com'; + ? user?.email || EMAIL_ADDRESSES.SYSTEM_FROM + : existingRsvp.guest_email || EMAIL_ADDRESSES.SYSTEM_FROM; // Handle organizer data from Supabase join const organizer = Array.isArray(eventDetails.users) ? eventDetails.users[0] : eventDetails.users; @@ -205,7 +206,7 @@ export async function PATCH( eventLocation: eventDetails.location, eventAddress: eventDetails.address || eventDetails.location, organizerName: organizerData?.full_name || 'Event Organizer', - organizerEmail: organizerData?.email || 'organizer@localloop.app', + organizerEmail: organizerData?.email || EMAIL_ADDRESSES.ORGANIZER, rsvpId: updatedRsvp.id, cancellationReason: updateData.notes || undefined, eventSlug: eventDetails.slug diff --git a/app/api/rsvps/route.ts b/app/api/rsvps/route.ts index e1211c4..c080e8b 100644 --- a/app/api/rsvps/route.ts +++ b/app/api/rsvps/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' import { createServerSupabaseClient } from '@/lib/supabase-server' import { sendRSVPConfirmationEmail } from '@/lib/email-service' +import { EMAIL_ADDRESSES } from '@/lib/config/email-addresses' import { z } from 'zod' // Performance optimization: Simple in-memory cache for RSVP checks (5 minutes) @@ -382,7 +383,7 @@ export async function POST(request: NextRequest) { eventLocation: event.location, eventAddress: event.location_details || event.location, organizerName: organizerData?.display_name || 'Event Organizer', - organizerEmail: organizerData?.email || 'organizer@localloop.app', + organizerEmail: organizerData?.email || EMAIL_ADDRESSES.ORGANIZER, rsvpId: newRsvp.id, guestCount: 1, isAuthenticated: !!user, diff --git a/app/contact/page.tsx b/app/contact/page.tsx index bdd3927..c3a8af6 100644 --- a/app/contact/page.tsx +++ b/app/contact/page.tsx @@ -1,5 +1,6 @@ import { Footer } from '@/components/ui/Footer'; import { Mail, Phone, MessageCircle } from 'lucide-react'; +import { EMAIL_ADDRESSES } from '@/lib/config/email-addresses'; export default function ContactPage() { return ( @@ -26,7 +27,7 @@ export default function ContactPage() {

Email

-

hello@localloop.events

+

{EMAIL_ADDRESSES.CONTACT}

diff --git a/lib/config/email-addresses.ts b/lib/config/email-addresses.ts new file mode 100644 index 0000000..62ad53d --- /dev/null +++ b/lib/config/email-addresses.ts @@ -0,0 +1,28 @@ +// Email address configuration - single source of truth for all LocalLoop emails +// Update the domain here to change all email addresses across the application + +const EMAIL_DOMAIN = 'localloopevents.xyz'; + +export const EMAIL_ADDRESSES = { + // System emails (automated messages) + SYSTEM_FROM: `noreply@${EMAIL_DOMAIN}`, + + // Contact and support emails + CONTACT: `hello@${EMAIL_DOMAIN}`, + SUPPORT: `support@${EMAIL_DOMAIN}`, + + // Organizational emails + ORGANIZER: `organizer@${EMAIL_DOMAIN}`, + + // Utility function to generate user emails + generateUserEmail: (userId: string) => `user-${userId}@${EMAIL_DOMAIN}`, +} as const; + +// Legacy aliases for backwards compatibility (if needed) +export const LEGACY_EMAIL_ADDRESSES = { + HELLO: EMAIL_ADDRESSES.CONTACT, + NOREPLY: EMAIL_ADDRESSES.SYSTEM_FROM, +} as const; + +// Export domain for other configurations +export const EMAIL_DOMAIN_CONFIG = EMAIL_DOMAIN; \ No newline at end of file diff --git a/lib/email-service.ts b/lib/email-service.ts index 533db65..f0f098b 100644 --- a/lib/email-service.ts +++ b/lib/email-service.ts @@ -6,6 +6,7 @@ import WelcomeEmail from './emails/welcome-email'; import EventReminderEmail from './emails/event-reminder'; import EventCancellationEmail from './emails/event-cancellation'; import RefundConfirmationEmail from './emails/templates/RefundConfirmationEmail'; +import { EMAIL_ADDRESSES } from './config/email-addresses'; // Lazy-initialize Resend to prevent build-time failures let resendInstance: Resend | null = null; @@ -20,16 +21,23 @@ function getResendInstance(): Resend { return resendInstance; } -// ✨ DEVELOPMENT MODE: Override email for testing with Resend free tier -const isDevelopment = process.env.NODE_ENV === 'development'; +// ✨ EMAIL OVERRIDE CONFIGURATION +// Use dedicated environment variable for email override control +const shouldOverrideEmails = process.env.OVERRIDE_EMAILS_TO_DEV === 'true'; +const isLocalDevelopment = process.env.NODE_ENV === 'development' && process.env.VERCEL_ENV !== 'production'; const devOverrideEmail = 'jackson_rhoden@outlook.com'; // Your verified email // Helper function to get the actual recipient email function getRecipientEmail(originalEmail: string): string { - if (isDevelopment && originalEmail !== devOverrideEmail) { + // Only override if explicitly enabled AND we're in local development + const shouldRedirect = shouldOverrideEmails && isLocalDevelopment; + + if (shouldRedirect && originalEmail !== devOverrideEmail) { console.log(`🔧 DEV MODE: Redirecting email from ${originalEmail} to ${devOverrideEmail}`); return devOverrideEmail; } + + // Always use original email in production or when override is disabled return originalEmail; } @@ -167,7 +175,7 @@ export async function sendRSVPConfirmationEmail( // Send the email const response = await getResendInstance().emails.send({ - from: process.env.RESEND_FROM_EMAIL || 'LocalLoop ', + from: process.env.RESEND_FROM_EMAIL || `LocalLoop <${EMAIL_ADDRESSES.SYSTEM_FROM}>`, to: getRecipientEmail(props.to), subject: `RSVP Confirmed: ${props.eventTitle}`, html: emailHtml, @@ -273,7 +281,7 @@ export async function sendWelcomeEmail( // Send the email const response = await getResendInstance().emails.send({ - from: process.env.RESEND_FROM_EMAIL || 'LocalLoop ', + from: process.env.RESEND_FROM_EMAIL || `LocalLoop <${EMAIL_ADDRESSES.SYSTEM_FROM}>`, to: getRecipientEmail(props.to), subject: 'Welcome to LocalLoop! 🎉', html: emailHtml, @@ -341,7 +349,7 @@ QUICK ACTIONS: NEED HELP GETTING STARTED? Visit your My Events page to manage your RSVPs and created events: ${baseUrl}/my-events -Have questions? Contact us at support@localloop.app +Have questions? Contact us at ${EMAIL_ADDRESSES.SUPPORT} --- This email was sent by LocalLoop. You're receiving this because you created an account. @@ -380,7 +388,7 @@ export async function sendEventReminderEmail( // Send the email const response = await getResendInstance().emails.send({ - from: process.env.RESEND_FROM_EMAIL || 'LocalLoop ', + from: process.env.RESEND_FROM_EMAIL || `LocalLoop <${EMAIL_ADDRESSES.SYSTEM_FROM}>`, to: getRecipientEmail(props.to), subject: getReminderSubject(), html: emailHtml, @@ -505,7 +513,7 @@ export async function sendEventCancellationEmail( // Send the email const response = await getResendInstance().emails.send({ - from: process.env.RESEND_FROM_EMAIL || 'LocalLoop ', + from: process.env.RESEND_FROM_EMAIL || `LocalLoop <${EMAIL_ADDRESSES.SYSTEM_FROM}>`, to: getRecipientEmail(props.to), subject: `Event Cancelled: ${props.eventTitle}`, html: emailHtml, @@ -589,7 +597,7 @@ QUICK ACTIONS: • Contact Organizer: ${props.organizerEmail} Questions about this cancellation? Contact the event organizer: ${props.organizerName} at ${props.organizerEmail} -${props.isTicketHolder ? `For refund inquiries, please contact: support@localloop.app\n` : ''} +${props.isTicketHolder ? `For refund inquiries, please contact: ${EMAIL_ADDRESSES.SUPPORT}\n` : ''} --- This cancellation notice was sent by LocalLoop on behalf of ${props.organizerName}. Unsubscribe: ${baseUrl}/unsubscribe?email=${encodeURIComponent(props.userEmail)} @@ -617,7 +625,7 @@ export async function sendRSVPCancellationEmail( // Send the email const response = await getResendInstance().emails.send({ - from: process.env.RESEND_FROM_EMAIL || 'LocalLoop ', + from: process.env.RESEND_FROM_EMAIL || `LocalLoop <${EMAIL_ADDRESSES.SYSTEM_FROM}>`, to: getRecipientEmail(props.to), subject: `RSVP Cancelled: ${props.eventTitle}`, html: emailHtml, @@ -741,7 +749,7 @@ export async function sendRefundConfirmationEmail( // Send the email const response = await getResendInstance().emails.send({ - from: process.env.RESEND_FROM_EMAIL || 'LocalLoop ', + from: process.env.RESEND_FROM_EMAIL || `LocalLoop <${EMAIL_ADDRESSES.SYSTEM_FROM}>`, to: getRecipientEmail(props.to), subject: subject, html: emailHtml, @@ -755,7 +763,7 @@ export async function sendRefundConfirmationEmail( { name: 'order', value: props.orderId } ], // Add reply-to support email for refund inquiries - replyTo: 'support@localloop.app', + replyTo: EMAIL_ADDRESSES.SUPPORT, }); console.log('Refund confirmation email sent successfully:', { @@ -829,7 +837,7 @@ REFUND INFORMATION: QUICK ACTIONS: • View Event Details: ${eventUrl} • Browse Other Events: ${baseUrl}/events -• Contact Support: support@localloop.app +• Contact Support: ${EMAIL_ADDRESSES.SUPPORT} ${isEventCancellation ? 'We apologize for any inconvenience caused by the event cancellation.' diff --git a/lib/emails/event-cancellation.tsx b/lib/emails/event-cancellation.tsx index cb1d7dc..01b88c3 100644 --- a/lib/emails/event-cancellation.tsx +++ b/lib/emails/event-cancellation.tsx @@ -216,7 +216,7 @@ export const EventCancellationEmail = ({ {isTicketHolder && ( - For refund inquiries, please contact: support@localloop.app + For refund inquiries, please contact: support@localloopevents.xyz )} diff --git a/lib/emails/send-ticket-confirmation.ts b/lib/emails/send-ticket-confirmation.ts index 52beca3..054fe87 100644 --- a/lib/emails/send-ticket-confirmation.ts +++ b/lib/emails/send-ticket-confirmation.ts @@ -1,5 +1,6 @@ import { Resend } from 'resend'; import { TicketConfirmationEmail } from './templates/TicketConfirmationEmail'; +import { EMAIL_ADDRESSES } from '../config/email-addresses'; // Lazy-initialize Resend to prevent build-time failures let resendInstance: Resend | null = null; @@ -14,16 +15,23 @@ function getResendInstance(): Resend { return resendInstance; } -// ✨ DEVELOPMENT MODE: Override email for testing with Resend free tier -const isDevelopment = process.env.NODE_ENV === 'development'; +// ✨ EMAIL OVERRIDE CONFIGURATION +// Use dedicated environment variable for email override control +const shouldOverrideEmails = process.env.OVERRIDE_EMAILS_TO_DEV === 'true'; +const isLocalDevelopment = process.env.NODE_ENV === 'development' && process.env.VERCEL_ENV !== 'production'; const devOverrideEmail = 'jackson_rhoden@outlook.com'; // Your verified email // Helper function to get the actual recipient email function getRecipientEmail(originalEmail: string): string { - if (isDevelopment && originalEmail !== devOverrideEmail) { + // Only override if explicitly enabled AND we're in local development + const shouldRedirect = shouldOverrideEmails && isLocalDevelopment; + + if (shouldRedirect && originalEmail !== devOverrideEmail) { console.log(`🔧 DEV MODE: Redirecting email from ${originalEmail} to ${devOverrideEmail}`); return devOverrideEmail; } + + // Always use original email in production or when override is disabled return originalEmail; } @@ -61,7 +69,7 @@ export async function sendTicketConfirmationEmail({ try { const resend = getResendInstance(); const { data, error } = await resend.emails.send({ - from: 'LocalLoop Events ', + from: process.env.RESEND_FROM_EMAIL || `LocalLoop <${EMAIL_ADDRESSES.SYSTEM_FROM}>`, to: [getRecipientEmail(to)], subject: `Ticket Confirmation - ${eventTitle}`, react: TicketConfirmationEmail({ diff --git a/lib/emails/welcome-email.tsx b/lib/emails/welcome-email.tsx index 86daa2c..733dcea 100644 --- a/lib/emails/welcome-email.tsx +++ b/lib/emails/welcome-email.tsx @@ -13,6 +13,7 @@ import { Heading, } from '@react-email/components'; import * as React from 'react'; +import { EMAIL_ADDRESSES } from '../config/email-addresses'; interface WelcomeEmailProps { userName: string; @@ -122,7 +123,7 @@ export const WelcomeEmail = ({ - Have questions? Contact us at support@localloop.app + Have questions? Contact us at {EMAIL_ADDRESSES.SUPPORT}
diff --git a/next.config.ts b/next.config.ts index e1d08f9..2c2ec5a 100644 --- a/next.config.ts +++ b/next.config.ts @@ -104,7 +104,7 @@ const nextConfig: NextConfig = { // Force consistent port and set dynamic environment env: { NEXT_PUBLIC_APP_URL: process.env.NODE_ENV === 'production' - ? 'https://local-loop-qa.vercel.app' + ? 'https://localloopevents.xyz' : 'http://localhost:3000', }, diff --git a/package-lock.json b/package-lock.json index f6e512a..c3a681a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,6 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", - "dotenv": "^16.5.0", "google-auth-library": "^9.15.1", "googleapis": "^149.0.0", "lucide-react": "^0.511.0", @@ -50,6 +49,7 @@ "@types/node": "^22.10.2", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", + "dotenv": "^16.5.0", "eslint": "^9.16.0", "eslint-config-next": "15.3.2", "husky": "^9.1.7", @@ -5820,6 +5820,7 @@ "version": "16.5.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=12" diff --git a/package.json b/package.json index 532d25a..862a636 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,6 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", - "dotenv": "^16.5.0", "google-auth-library": "^9.15.1", "googleapis": "^149.0.0", "lucide-react": "^0.511.0", @@ -91,6 +90,7 @@ "@types/node": "^22.10.2", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", + "dotenv": "^16.5.0", "eslint": "^9.16.0", "eslint-config-next": "15.3.2", "husky": "^9.1.7", diff --git a/tests/load/README.md b/tests/load/README.md index a9088dd..6602cd0 100644 --- a/tests/load/README.md +++ b/tests/load/README.md @@ -78,10 +78,10 @@ Set the base URL for different environments: BASE_URL=http://localhost:3000 npm run load-test # Staging environment -BASE_URL=https://staging.localloop.app npm run load-test +BASE_URL=https://staging.localloopevents.xyz npm run load-test # Production (use with extreme caution!) -BASE_URL=https://localloop.app npm run load-test +BASE_URL=https://localloopevents.xyz npm run load-test ``` ## 📈 Understanding Results