diff --git a/api/_utils/email.ts b/api/_utils/email.ts new file mode 100644 index 0000000..182dcaa --- /dev/null +++ b/api/_utils/email.ts @@ -0,0 +1,367 @@ +import { Resend } from 'resend'; + +const resend = new Resend(process.env.RESEND_API_KEY); + +export interface UnitData { + name: string; + birthDate: string; + birthTime: string; + birthPlace: string; +} + +export interface SupportRequest { + name: string; + email: string; + subject: string; + message: string; + orderRef?: string; +} + +export interface ResendManualRequest { + email: string; + manualUrl: string; + unitA?: UnitData; + unitB?: UnitData; +} + +export interface EmailOptions { + type: string; + to?: string; + unitA?: UnitData; + unitB?: UnitData; + manualUrl?: string; + supportRequest?: SupportRequest; + resendRequest?: ResendManualRequest; +} + +export async function sendEmail(options: EmailOptions) { + const { type, to, unitA, unitB, manualUrl, supportRequest, resendRequest } = options; + + let subject: string; + let html: string; + let recipients: string[]; + let replyTo: string | undefined; + + switch (type) { + case 'purchase_confirmation': + if (!to) throw new Error('Missing recipient'); + subject = 'DEFRAG // YOUR MANUAL IS READY'; + html = generatePurchaseEmail(unitA!, unitB!, manualUrl!); + recipients = [to]; + break; + + case 'manual_delivery': + if (!to) throw new Error('Missing recipient'); + subject = 'DEFRAG // MANUAL GENERATED'; + html = generateManualDeliveryEmail(unitA!, unitB!, manualUrl!); + recipients = [to]; + break; + + case 'welcome': + if (!to) throw new Error('Missing recipient'); + subject = 'DEFRAG // SYSTEM INITIALIZATION'; + html = generateWelcomeEmail(unitA?.name); + recipients = [to]; + break; + + case 'resend_manual': + if (!resendRequest?.email) throw new Error('Missing email'); + subject = 'DEFRAG // MANUAL RECOVERY'; + html = generateResendManualEmail(resendRequest); + recipients = [resendRequest.email]; + break; + + case 'support_confirmation': + if (!supportRequest?.email) throw new Error('Missing support request'); + subject = 'DEFRAG // SUPPORT REQUEST RECEIVED'; + html = generateSupportConfirmationEmail(supportRequest); + recipients = [supportRequest.email]; + break; + + case 'support_internal': + if (!supportRequest) throw new Error('Missing support request'); + subject = `DEFRAG SUPPORT // ${supportRequest.subject}`; + html = generateSupportInternalEmail(supportRequest); + recipients = ['info@defrag.app']; // Internal notification + replyTo = supportRequest.email; + break; + + default: + throw new Error('Invalid email type'); + } + + const { data, error } = await resend.emails.send({ + from: 'DEFRAG ', + to: recipients, + subject, + html, + replyTo, + }); + + if (error) { + throw new Error(error.message); + } + + return data; +} + +// ============================================ +// EMAIL TEMPLATES +// ============================================ + +function emailWrapper(content: string): string { + return ` + + + + + + DEFRAG + + + + + + + +
+ + + + + + + + + + + + + + + +
+ DEFRAG +
+ ${content} +
+

+ DEFRAG · User Manuals for Your People
+ defrag.app +

+
+
+ + + `.trim(); +} + +// WELCOME EMAIL +function generateWelcomeEmail(name?: string): string { + const greeting = name ? name : 'there'; + return emailWrapper(` +

+ Welcome to DEFRAG +

+ +

+ Hi ${greeting}, you're in! DEFRAG creates relationship operating manuals—practical guides that help you understand and connect with the people who matter most. +

+ +
+

Here's how it works:

+

1. Enter basic info for two people

+

2. We analyze the patterns and dynamics

+

3. Get your personalized manual with insights and scripts

+
+ + + + + +
+ + Create Your Manual + +
+ +

+ Questions? Reply to this email or visit defrag.app/how-it-works +

+ `); +} + +// RESEND MANUAL EMAIL (Forgot Password equivalent) +function generateResendManualEmail(request: ResendManualRequest): string { + const unitNames = request.unitA && request.unitB + ? `${request.unitA.name} & ${request.unitB.name}` + : 'Your Manual'; + + return emailWrapper(` +

+ Here's Your Manual +

+ +

+ You requested access to your relationship operating manual. Click below to view it. +

+ +
+

Manual

+

${unitNames}

+
+ + + + + +
+ + Access Manual + +
+ +

+ Didn't request this? You can safely ignore this email. +

+ +

+ Need help? Contact info@defrag.app +

+ `); +} + +// SUPPORT CONFIRMATION EMAIL (to user) +function generateSupportConfirmationEmail(request: SupportRequest): string { + return emailWrapper(` +

+ We Got Your Message +

+ +

+ Thanks for reaching out. We'll get back to you within 24 hours. +

+ +
+

Your Request

+ + + + + + ${request.orderRef ? ` + + + + + ` : ''} +
Subject:${request.subject}
Order:${request.orderRef}
+
+

Message:

+

${request.message}

+
+
+ +

+ Reply to this email to add more information. +

+ `); +} + +// SUPPORT INTERNAL EMAIL (to info@defrag.app) +function generateSupportInternalEmail(request: SupportRequest): string { + return emailWrapper(` +

+ New Support Request +

+ +
+ + + + + + + + + + + + + + ${request.orderRef ? ` + + + + + ` : ''} +
From:${request.name}
Email: + ${request.email} +
Subject:${request.subject}
Order:${request.orderRef}
+
+ +
+

Message

+

${request.message}

+
+ +

+ Reply directly to respond to the user. +

+ `); +} + +// PURCHASE CONFIRMATION EMAIL +function generatePurchaseEmail(unitA: UnitData, unitB: UnitData, manualUrl: string): string { + return emailWrapper(` +

+ Your Manual is Ready +

+ +

+ Your personalized relationship operating manual has been generated and is ready to view. +

+ + + + + + + + +
+

Person 1

+

${unitA?.name || 'Unknown'}

+
+

Person 2

+

${unitB?.name || 'Unknown'}

+
+ + + + + + +
+ + View Your Manual + +
+ +

+ This link will remain active. Bookmark it for easy access. +

+ +

+ Need help? Contact info@defrag.app +

+ `); +} + +function generateManualDeliveryEmail(unitA: UnitData, unitB: UnitData, manualUrl: string): string { + return generatePurchaseEmail(unitA, unitB, manualUrl); +} diff --git a/api/send-email.ts b/api/send-email.ts index 97a7ca1..9a56ecd 100644 --- a/api/send-email.ts +++ b/api/send-email.ts @@ -1,29 +1,5 @@ import type { VercelRequest, VercelResponse } from '@vercel/node'; -import { Resend } from 'resend'; - -const resend = new Resend(process.env.RESEND_API_KEY); - -interface UnitData { - name: string; - birthDate: string; - birthTime: string; - birthPlace: string; -} - -interface SupportRequest { - name: string; - email: string; - subject: string; - message: string; - orderRef?: string; -} - -interface ResendManualRequest { - email: string; - manualUrl: string; - unitA?: UnitData; - unitB?: UnitData; -} +import { sendEmail } from './_utils/email'; export default async function handler(req: VercelRequest, res: VercelResponse) { if (req.method !== 'POST') { @@ -37,336 +13,19 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { return res.status(400).json({ error: 'Missing email type' }); } - let subject: string; - let html: string; - let recipients: string[]; - let replyTo: string | undefined; - - switch (type) { - case 'purchase_confirmation': - if (!to) return res.status(400).json({ error: 'Missing recipient' }); - subject = 'DEFRAG // YOUR MANUAL IS READY'; - html = generatePurchaseEmail(unitA, unitB, manualUrl); - recipients = [to]; - break; - - case 'manual_delivery': - if (!to) return res.status(400).json({ error: 'Missing recipient' }); - subject = 'DEFRAG // MANUAL GENERATED'; - html = generateManualDeliveryEmail(unitA, unitB, manualUrl); - recipients = [to]; - break; - - case 'welcome': - if (!to) return res.status(400).json({ error: 'Missing recipient' }); - subject = 'DEFRAG // SYSTEM INITIALIZATION'; - html = generateWelcomeEmail(unitA?.name); - recipients = [to]; - break; - - case 'resend_manual': - if (!resendRequest?.email) return res.status(400).json({ error: 'Missing email' }); - subject = 'DEFRAG // MANUAL RECOVERY'; - html = generateResendManualEmail(resendRequest); - recipients = [resendRequest.email]; - break; - - case 'support_confirmation': - if (!supportRequest?.email) return res.status(400).json({ error: 'Missing support request' }); - subject = 'DEFRAG // SUPPORT REQUEST RECEIVED'; - html = generateSupportConfirmationEmail(supportRequest); - recipients = [supportRequest.email]; - break; - - case 'support_internal': - if (!supportRequest) return res.status(400).json({ error: 'Missing support request' }); - subject = `DEFRAG SUPPORT // ${supportRequest.subject}`; - html = generateSupportInternalEmail(supportRequest); - recipients = ['info@defrag.app']; // Internal notification - replyTo = supportRequest.email; - break; - - default: - return res.status(400).json({ error: 'Invalid email type' }); - } - - const { data, error } = await resend.emails.send({ - from: 'DEFRAG ', - to: recipients, - subject, - html, - replyTo, + const data = await sendEmail({ + type, + to, + unitA, + unitB, + manualUrl, + supportRequest, + resendRequest }); - if (error) { - console.error('Resend error:', error); - return res.status(500).json({ error: error.message }); - } - res.status(200).json({ success: true, id: data?.id }); } catch (error: any) { console.error('Send email error:', error); res.status(500).json({ error: error.message }); } } - -// ============================================ -// EMAIL TEMPLATES -// ============================================ - -function emailWrapper(content: string): string { - return ` - - - - - - DEFRAG - - - - - - - -
- - - - - - - - - - - - - - - -
- DEFRAG -
- ${content} -
-

- DEFRAG · User Manuals for Your People
- defrag.app -

-
-
- - - `.trim(); -} - -// WELCOME EMAIL -function generateWelcomeEmail(name?: string): string { - const greeting = name ? name : 'there'; - return emailWrapper(` -

- Welcome to DEFRAG -

- -

- Hi ${greeting}, you're in! DEFRAG creates relationship operating manuals—practical guides that help you understand and connect with the people who matter most. -

- -
-

Here's how it works:

-

1. Enter basic info for two people

-

2. We analyze the patterns and dynamics

-

3. Get your personalized manual with insights and scripts

-
- - - - - -
- - Create Your Manual - -
- -

- Questions? Reply to this email or visit defrag.app/how-it-works -

- `); -} - -// RESEND MANUAL EMAIL (Forgot Password equivalent) -function generateResendManualEmail(request: ResendManualRequest): string { - const unitNames = request.unitA && request.unitB - ? `${request.unitA.name} & ${request.unitB.name}` - : 'Your Manual'; - - return emailWrapper(` -

- Here's Your Manual -

- -

- You requested access to your relationship operating manual. Click below to view it. -

- -
-

Manual

-

${unitNames}

-
- - - - - -
- - Access Manual - -
- -

- Didn't request this? You can safely ignore this email. -

- -

- Need help? Contact info@defrag.app -

- `); -} - -// SUPPORT CONFIRMATION EMAIL (to user) -function generateSupportConfirmationEmail(request: SupportRequest): string { - return emailWrapper(` -

- We Got Your Message -

- -

- Thanks for reaching out. We'll get back to you within 24 hours. -

- -
-

Your Request

- - - - - - ${request.orderRef ? ` - - - - - ` : ''} -
Subject:${request.subject}
Order:${request.orderRef}
-
-

Message:

-

${request.message}

-
-
- -

- Reply to this email to add more information. -

- `); -} - -// SUPPORT INTERNAL EMAIL (to info@defrag.app) -function generateSupportInternalEmail(request: SupportRequest): string { - return emailWrapper(` -

- New Support Request -

- -
- - - - - - - - - - - - - - ${request.orderRef ? ` - - - - - ` : ''} -
From:${request.name}
Email: - ${request.email} -
Subject:${request.subject}
Order:${request.orderRef}
-
- -
-

Message

-

${request.message}

-
- -

- Reply directly to respond to the user. -

- `); -} - -// PURCHASE CONFIRMATION EMAIL -function generatePurchaseEmail(unitA: UnitData, unitB: UnitData, manualUrl: string): string { - return emailWrapper(` -

- Your Manual is Ready -

- -

- Your personalized relationship operating manual has been generated and is ready to view. -

- - - - - - - - -
-

Person 1

-

${unitA?.name || 'Unknown'}

-
-

Person 2

-

${unitB?.name || 'Unknown'}

-
- - - - - - -
- - View Your Manual - -
- -

- This link will remain active. Bookmark it for easy access. -

- -

- Need help? Contact info@defrag.app -

- `); -} - -function generateManualDeliveryEmail(unitA: UnitData, unitB: UnitData, manualUrl: string): string { - return generatePurchaseEmail(unitA, unitB, manualUrl); -} diff --git a/api/stripe-webhook.ts b/api/stripe-webhook.ts index 38c6687..2c20360 100644 --- a/api/stripe-webhook.ts +++ b/api/stripe-webhook.ts @@ -1,7 +1,9 @@ import type { VercelRequest, VercelResponse } from '@vercel/node'; +import { waitUntil } from '@vercel/functions'; import Stripe from 'stripe'; import { db } from '../src/lib/firebase'; import { doc, updateDoc, setDoc } from 'firebase/firestore'; +import { sendEmail } from './_utils/email'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: '2024-12-18.acacia', @@ -25,12 +27,6 @@ async function buffer(readable: any) { } export default async function handler(req: VercelRequest, res: VercelResponse) { - // Only allow POST requests - if (req.method !== 'POST') { - res.setHeader('Allow', 'POST'); - return res.status(405).end('Method Not Allowed'); - } - // Add CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'POST'); @@ -40,6 +36,12 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { return res.status(200).end(); } + // Only allow POST requests + if (req.method !== 'POST') { + res.setHeader('Allow', 'POST'); + return res.status(405).end('Method Not Allowed'); + } + try { // Get the raw body const buf = await buffer(req); @@ -68,14 +70,14 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { // Handle the event switch (event.type) { case 'checkout.session.completed': { - const session = event.data.object as Stripe.Checkout.Session; - console.log('💰 Payment successful:', session.id); + const eventSession = event.data.object as Stripe.Checkout.Session; + console.log('💰 Payment successful:', eventSession.id); // Extract customer info - const customerEmail = session.customer_email || session.customer_details?.email; - const sessionId = session.id; - const customerId = session.customer as string; - const paymentIntentId = session.payment_intent as string; + const customerEmail = eventSession.customer_email || eventSession.customer_details?.email; + const sessionId = eventSession.id; + const customerId = eventSession.customer as string; + const paymentIntentId = eventSession.payment_intent as string; if (!customerEmail) { console.error('❌ No customer email found in session'); @@ -85,6 +87,18 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { console.log('📧 Customer email:', customerEmail); console.log('🎫 Session ID:', sessionId); + // Retrieve full session to ensure we have metadata + let unitA, unitB; + try { + const session = await stripe.checkout.sessions.retrieve(sessionId); + unitA = session.metadata?.unitA ? JSON.parse(session.metadata.unitA) : undefined; + unitB = session.metadata?.unitB ? JSON.parse(session.metadata.unitB) : undefined; + } catch (err) { + console.error('⚠️ Failed to retrieve full session or parse metadata:', err); + } + + const manualUrl = `https://defrag.app/manual?session_id=${sessionId}`; + // Update user's payment status in Firestore try { // Find user by email @@ -102,22 +116,18 @@ export default async function handler(req: VercelRequest, res: VercelResponse) { console.log('✅ Updated payment status for:', customerEmail); - // Send confirmation email - try { - await fetch(`${process.env.VERCEL_URL || 'http://localhost:3000'}/api/send-email`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - type: 'purchase_confirmation', - to: customerEmail, - sessionId: sessionId, - }), - }); - console.log('📧 Confirmation email sent to:', customerEmail); - } catch (emailErr) { - console.error('❌ Failed to send confirmation email:', emailErr); - // Don't fail the webhook if email fails - } + // Send confirmation email (Non-blocking) + waitUntil( + sendEmail({ + type: 'purchase_confirmation', + to: customerEmail, + unitA, + unitB, + manualUrl, + }) + .then(() => console.log('📧 Confirmation email queued for:', customerEmail)) + .catch((emailErr) => console.error('❌ Failed to send confirmation email:', emailErr)) + ); } catch (firestoreErr) { console.error('❌ Failed to update Firestore:', firestoreErr); diff --git a/package-lock.json b/package-lock.json index dafb53d..809c82e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@google/genai": "^1.38.0", "@stripe/stripe-js": "^8.6.3", + "@vercel/functions": "^3.4.1", "astronomy-engine": "^2.1.19", "clsx": "^2.0.0", "date-fns": "^2.30.0", @@ -2967,6 +2968,26 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/@vercel/functions": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@vercel/functions/-/functions-3.4.1.tgz", + "integrity": "sha512-+wqs1fscC1l2NZYL8Ahxe/vba4YzcuhxTV99Kf8sNwNeATaxQ9rbR6BILksoGDmm99YiD0wc0hBJcYxVNwiNaw==", + "license": "Apache-2.0", + "dependencies": { + "@vercel/oidc": "3.1.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-web-identity": "*" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-provider-web-identity": { + "optional": true + } + } + }, "node_modules/@vercel/nft": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-1.1.1.tgz", @@ -3057,6 +3078,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@vercel/oidc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.1.0.tgz", + "integrity": "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==", + "license": "Apache-2.0", + "engines": { + "node": ">= 20" + } + }, "node_modules/@vercel/static-config": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@vercel/static-config/-/static-config-3.1.2.tgz", diff --git a/package.json b/package.json index 45f985d..8f173dd 100644 --- a/package.json +++ b/package.json @@ -13,20 +13,21 @@ "dependencies": { "@google/genai": "^1.38.0", "@stripe/stripe-js": "^8.6.3", + "@vercel/functions": "^3.4.1", "astronomy-engine": "^2.1.19", + "clsx": "^2.0.0", + "date-fns": "^2.30.0", "firebase": "^12.8.0", + "firebase-admin": "^12.0.0", + "lucide-react": "^0.469.0", + "micro": "^10.0.1", "react": "^19.2.3", "react-dom": "^19.2.3", + "react-hot-toast": "^2.4.0", "react-router-dom": "^7.12.0", - "resend": "^6.8.0", - "stripe": "^20.2.0", "recharts": "^2.15.0", - "lucide-react": "^0.469.0", - "clsx": "^2.0.0", - "react-hot-toast": "^2.4.0", - "date-fns": "^2.30.0", - "micro": "^10.0.1", - "firebase-admin": "^12.0.0" + "resend": "^6.8.0", + "stripe": "^20.2.0" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.18", @@ -39,4 +40,4 @@ "typescript": "~5.8.2", "vite": "^6.2.0" } -} \ No newline at end of file +}