diff --git a/src/api/emailClient.ts b/src/api/emailClient.ts new file mode 100644 index 0000000..47b3bb6 --- /dev/null +++ b/src/api/emailClient.ts @@ -0,0 +1,145 @@ +import axios from 'axios'; +import { logger } from '../utils/logger'; + +// Get API base URL from runtime config (window.env) or build-time config +// IMPORTANT: Call this function at REQUEST TIME, not at module load time, +// to ensure window.env is populated by env-config.js +const getApiBaseUrl = (): string => { + // Check window.env first (runtime config from env.sh) + if (typeof window !== 'undefined' && window.env?.API_BASE_URL) { + return window.env.API_BASE_URL; + } + + // Fall back to build-time env or production default + return import.meta.env.VITE_API_BASE_URL || 'https://puffinoffset.com/api'; +}; + +export interface ContactEmailData { + name: string; + email: string; + phone?: string; + company?: string; + message: string; +} + +export interface ReceiptEmailData { + customerEmail: string; + orderDetails: { + tons: number; + portfolioId: number; + baseAmount: number; + processingFee: number; + totalAmount: number; + orderId: string; + stripeSessionId: string; + }; +} + +export interface AdminNotificationData { + orderDetails: { + tons: number; + portfolioId: number; + totalAmount: number; + orderId: string; + }; + customerEmail: string; +} + +export interface EmailResponse { + success: boolean; + messageId: string; + message: string; + skipped?: boolean; +} + +/** + * Send contact form email to admin + * @param data Contact form data + * @returns Email response with message ID + */ +export async function sendContactEmail(data: ContactEmailData): Promise { + try { + const apiBaseUrl = getApiBaseUrl(); + logger.info('Sending contact form email:', { name: data.name, email: data.email }); + logger.info('Using API base URL:', apiBaseUrl); + + const response = await axios.post( + `${apiBaseUrl}/api/email/contact`, + data + ); + + logger.info('Contact email sent successfully:', response.data.messageId); + return response.data; + } catch (error) { + if (axios.isAxiosError(error)) { + logger.error('Contact email failed:', error.response?.data || error.message); + throw new Error(error.response?.data?.error || 'Failed to send contact form'); + } + throw error; + } +} + +/** + * Send receipt email to customer + * @param data Receipt email data with order details + * @returns Email response with message ID + */ +export async function sendReceiptEmail(data: ReceiptEmailData): Promise { + try { + const apiBaseUrl = getApiBaseUrl(); + logger.info('Sending receipt email to:', data.customerEmail); + logger.info('Order ID:', data.orderDetails.orderId); + + const response = await axios.post( + `${apiBaseUrl}/api/email/receipt`, + data + ); + + logger.info('Receipt email sent successfully:', response.data.messageId); + return response.data; + } catch (error) { + if (axios.isAxiosError(error)) { + logger.error('Receipt email failed:', error.response?.data || error.message); + throw new Error(error.response?.data?.error || 'Failed to send receipt email'); + } + throw error; + } +} + +/** + * Send admin notification for new order + * @param data Admin notification data + * @returns Email response with message ID + */ +export async function sendAdminNotification( + data: AdminNotificationData +): Promise { + try { + const apiBaseUrl = getApiBaseUrl(); + logger.info('Sending admin notification for order:', data.orderDetails.orderId); + + const response = await axios.post( + `${apiBaseUrl}/api/email/admin-notify`, + data + ); + + if (response.data.skipped) { + logger.info('Admin notification skipped (disabled in config)'); + } else { + logger.info('Admin notification sent successfully:', response.data.messageId); + } + return response.data; + } catch (error) { + if (axios.isAxiosError(error)) { + logger.error('Admin notification failed:', error.response?.data || error.message); + // Don't throw - admin notifications should not block user experience + logger.warn('Admin notification failure is non-fatal, continuing...'); + return { + success: false, + messageId: '', + message: 'Admin notification failed (non-fatal)', + }; + } + throw error; + } +} diff --git a/src/components/Contact.tsx b/src/components/Contact.tsx index 633e369..bd012de 100644 --- a/src/components/Contact.tsx +++ b/src/components/Contact.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { Mail, Phone, Loader2, AlertCircle } from 'lucide-react'; -import { validateEmail, sendFormspreeEmail } from '../utils/email'; +import { validateEmail, sendContactFormEmail } from '../utils/email'; import { analytics } from '../utils/analytics'; export function Contact() { @@ -26,8 +26,8 @@ export function Contact() { throw new Error('Please enter a valid email address'); } - // Send via Formspree - await sendFormspreeEmail(formData, 'contact'); + // Send via SMTP backend + await sendContactFormEmail(formData, 'contact'); setSubmitted(true); analytics.event('contact', 'form_submitted'); diff --git a/src/components/MobileOffsetOrder.tsx b/src/components/MobileOffsetOrder.tsx index 03de30b..0beded2 100644 --- a/src/components/MobileOffsetOrder.tsx +++ b/src/components/MobileOffsetOrder.tsx @@ -5,7 +5,7 @@ import { createOffsetOrder, getPortfolios } from '../api/wrenClient'; import type { OffsetOrder as OffsetOrderType, Portfolio, OffsetProject } from '../types'; import { currencies, formatCurrency } from '../utils/currencies'; import { config } from '../utils/config'; -import { sendFormspreeEmail } from '../utils/email'; +import { sendContactFormEmail } from '../utils/email'; import { RadialProgress } from './RadialProgress'; import { PortfolioDonutChart } from './PortfolioDonutChart'; import { getProjectColor } from '../utils/portfolioColors'; @@ -248,7 +248,7 @@ export function MobileOffsetOrder({ tons, monetaryAmount, onBack }: Props) { e.preventDefault(); setLoading(true); try { - await sendFormspreeEmail(formData, 'offset'); + await sendContactFormEmail(formData, 'contact'); setSuccess(true); setCurrentStep('confirmation'); } catch (err) { diff --git a/src/components/OffsetOrder.tsx b/src/components/OffsetOrder.tsx index 8032af6..fa518b7 100644 --- a/src/components/OffsetOrder.tsx +++ b/src/components/OffsetOrder.tsx @@ -6,7 +6,7 @@ import { createCheckoutSession } from '../api/checkoutClient'; import type { OffsetOrder as OffsetOrderType, Portfolio, OffsetProject } from '../types'; import { currencies, formatCurrency } from '../utils/currencies'; import { config } from '../utils/config'; -import { sendFormspreeEmail } from '../utils/email'; +import { sendContactFormEmail } from '../utils/email'; import { logger } from '../utils/logger'; import { FormInput } from './forms/FormInput'; import { FormTextarea } from './forms/FormTextarea'; @@ -247,7 +247,7 @@ export function OffsetOrder({ tons, monetaryAmount, onBack, calculatorType }: Pr e.preventDefault(); setLoading(true); try { - await sendFormspreeEmail(formData, 'offset'); + await sendContactFormEmail(formData, 'contact'); setSuccess(true); } catch (err) { setError('Failed to send request. Please try again.'); diff --git a/src/utils/config.ts b/src/utils/config.ts index ca40bf0..4d45156 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,7 +1,5 @@ interface Config { wrenApiKey: string; - formspreeContactId: string; - formspreeOffsetId: string; isProduction: boolean; } @@ -31,14 +29,10 @@ const getEnv = (key: string): string => { // Load environment variables const wrenApiKey = getEnv('VITE_WREN_API_TOKEN'); -const formspreeContactId = getEnv('VITE_FORMSPREE_CONTACT_ID'); -const formspreeOffsetId = getEnv('VITE_FORMSPREE_OFFSET_ID'); // Initialize config export const config: Config = { wrenApiKey: wrenApiKey || '', - formspreeContactId: formspreeContactId || 'xkgovnby', - formspreeOffsetId: formspreeOffsetId || 'xvgzbory', isProduction: import.meta.env.PROD === true }; diff --git a/src/utils/email.ts b/src/utils/email.ts index 7bbe506..7bd499d 100644 --- a/src/utils/email.ts +++ b/src/utils/email.ts @@ -1,4 +1,5 @@ import { analytics } from './analytics'; +import { sendContactEmail, type ContactEmailData } from '../api/emailClient'; interface EmailData { name: string; @@ -14,7 +15,7 @@ export function validateEmail(email: string): boolean { } export function formatEmailContent(data: EmailData, type: 'contact' | 'offset'): { subject: string, body: string } { - const subject = type === 'contact' + const subject = type === 'contact' ? `Contact from ${data.name} - Puffin Offset` : `Offset Request - ${data.name}`; @@ -36,32 +37,22 @@ export function sendEmail(to: string, subject: string, body: string): void { window.location.href = mailtoUrl; } -export async function sendFormspreeEmail(data: EmailData, type: 'contact' | 'offset'): Promise { - const FORMSPREE_CONTACT_ID = 'xkgovnby'; // Contact form - const FORMSPREE_OFFSET_ID = 'xvgzbory'; // Offset request form - - const formId = type === 'contact' ? FORMSPREE_CONTACT_ID : FORMSPREE_OFFSET_ID; - +/** + * Send contact form email via SMTP backend + * @param data Contact form data + * @param type Email type (contact or offset) - currently only 'contact' is supported + */ +export async function sendContactFormEmail(data: EmailData, type: 'contact' | 'offset' = 'contact'): Promise { try { - const response = await fetch(`https://formspree.io/f/${formId}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }, - body: JSON.stringify({ - ...data, - _subject: type === 'contact' - ? `Contact from ${data.name} - Puffin Offset` - : `Offset Request - ${data.name}` - }), - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.error || 'Failed to send message'); - } + const contactData: ContactEmailData = { + name: data.name, + email: data.email, + phone: data.phone, + company: data.company, + message: data.message, + }; + await sendContactEmail(contactData); analytics.event('email', 'sent', type); } catch (error) { analytics.error(error as Error, 'Email sending failed'); diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 34b07e2..93030bc 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -2,8 +2,6 @@ interface ImportMetaEnv { readonly VITE_WREN_API_TOKEN: string; - readonly VITE_FORMSPREE_CONTACT_ID: string; - readonly VITE_FORMSPREE_OFFSET_ID: string; readonly PROD: boolean; readonly DEV: boolean; // Add index signature for dynamic access @@ -19,7 +17,5 @@ interface Window { env?: { [key: string]: string | undefined; WREN_API_TOKEN?: string; - FORMSPREE_CONTACT_ID?: string; - FORMSPREE_OFFSET_ID?: string; }; }