diff --git a/package-lock.json b/package-lock.json index 8c2ec2249e..9ee245a17c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -62,6 +62,7 @@ "notion-client": "^6.16.0", "notion-utils": "^6.16.0", "posthog-js": "^1.96.1", + "postmark": "^4.0.2", "puppeteer": "^19.11.1", "raw-body": "^2.5.2", "react": "18.2.0", @@ -5582,6 +5583,16 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", @@ -8561,9 +8572,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -14147,6 +14158,14 @@ "fflate": "^0.4.1" } }, + "node_modules/postmark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postmark/-/postmark-4.0.2.tgz", + "integrity": "sha512-2zlCv+KVVQ0KoamXZHE7d+gXzLlr8tPE+PxQmtUaIZhbHzZAq4D6yH2b+ykhA8wYCc5ISodcx8U1aNLenXBs9g==", + "dependencies": { + "axios": "^1.6.2" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -22252,6 +22271,16 @@ "integrity": "sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg==", "dev": true }, + "axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "requires": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "axobject-query": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz", @@ -24450,9 +24479,9 @@ "dev": true }, "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==" }, "for-each": { "version": "0.3.3", @@ -27932,6 +27961,14 @@ "fflate": "^0.4.1" } }, + "postmark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postmark/-/postmark-4.0.2.tgz", + "integrity": "sha512-2zlCv+KVVQ0KoamXZHE7d+gXzLlr8tPE+PxQmtUaIZhbHzZAq4D6yH2b+ykhA8wYCc5ISodcx8U1aNLenXBs9g==", + "requires": { + "axios": "^1.6.2" + } + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/package.json b/package.json index 5412ce336c..60a182ccf7 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "notion-client": "^6.16.0", "notion-utils": "^6.16.0", "posthog-js": "^1.96.1", + "postmark": "^4.0.2", "puppeteer": "^19.11.1", "raw-body": "^2.5.2", "react": "18.2.0", diff --git a/pages/api/cron/update-pro-accounts-plan.ts b/pages/api/cron/update-pro-accounts-plan.ts index d612bf56f2..5898688701 100644 --- a/pages/api/cron/update-pro-accounts-plan.ts +++ b/pages/api/cron/update-pro-accounts-plan.ts @@ -8,6 +8,7 @@ import { import dayjs from 'dayjs'; import timezone from 'dayjs/plugin/timezone'; import utc from 'dayjs/plugin/utc'; +import { ServerClient } from 'postmark'; dayjs.extend(utc); dayjs.extend(timezone); @@ -41,37 +42,36 @@ const handler = async (req: Request): Promise => { } }; -const sendOutTrialEndEmail = async (userEmail: string) => { - const response = await fetch('https://api.sendgrid.com/v3/mail/send', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${process.env.SENDGRID_API_KEY}`, - }, - body: JSON.stringify({ - from: { - email: 'team@chateverywhere.app', - name: 'Chat Everywhere Team', - }, - reply_to: { - email: 'jack@exploratorlabs.com', - name: 'Jack', - }, - personalizations: [ - { - to: [ - { - email: userEmail, - }, - ], - }, - ], - template_id: 'd-e5fff0aa9b5948b4871c436812392134', - }), - }); +const postmarkClient = new ServerClient(process.env.POSTMARK_SERVER_TOKEN || ''); - if (!response.ok) { - throw new Error(`Failed to send email: ${response.statusText}`); +const sendOutTrialEndEmail = async (userEmail: string) => { + try { + const response = await postmarkClient.sendEmailWithTemplate({ + From: "Chat Everywhere Team ", + ReplyTo: 'Jack ', + To: userEmail, + TemplateId: 35875013, // Replace with your actual numeric template ID + TemplateModel: { + // The properties here should match the variables in your Postmark template + "product_url": "product_url_Value", + "product_name": "Chat Everywhere", + "action_url": "action_url_Value", + "trial_extension_url": "trial_extension_url_Value", + "feedback_url": "feedback_url_Value", + "export_url": "export_url_Value", + "close_account_url": "close_account_url_Value", + "sender_name": "sender_name_Value", + "company_name": "company_name_Value", + "company_address": "company_address_Value" + } + }); + console.log('Email sent successfully:', response); + } catch (error) { + if (error instanceof Error) { + console.error('Failed to send email:', error.message); + } else { + console.error('An unexpected error occurred:', error); + } } }; diff --git a/pages/api/webhooks/postmark.ts b/pages/api/webhooks/postmark.ts new file mode 100644 index 0000000000..49f46826cb --- /dev/null +++ b/pages/api/webhooks/postmark.ts @@ -0,0 +1,79 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import { serverSideTrackEvent } from '@/utils/app/eventTracking'; +import { getAdminSupabaseClient } from '@/utils/server/supabase'; + +type PostmarkPayload = { + RecordType: string; + MessageStream: string; + FirstOpen: boolean; + Client: { + Name: string; + Company: string; + Family: string; + }; + OS: { + Name: string; + Company: string; + Family: string; + }; + Platform: string; + UserAgent: string; + Geo: { + CountryISOCode: string; + Country: string; + RegionISOCode: string; + Region: string; + City: string; + Zip: string; + Coords: string; + IP: string; + }; + MessageID: string; + Metadata: { + [key: string]: string; + }; + ReceivedAt: string; + Tag: string; + Recipient: string; +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const payload: PostmarkPayload = req.body; + + // Verifying the authenticity of request + if (req.method !== 'POST') { + res.status(405).end('Method Not Allowed'); + } + const tokenHeaderName = 'x-custom-auth-token'; + const expectedToken = process.env.POSTMARK_WEBHOOK_KEY; + const providedToken = req.headers[tokenHeaderName]; + // Check if the token matches the expected value + if (!providedToken || providedToken !== expectedToken) { + res.status(401).end('Unauthorized'); + return; + } + + // Check if the RecordType is 'Open' + if (payload.RecordType === 'Open') { + const supabase = getAdminSupabaseClient(); + // Fetch the user from your database using the recipient's email + const { data: user, error } = await supabase + .from('profiles') + .select('id, email') + .eq('email', payload.Recipient) + .single(); + // If a user is found, track the event and log it + if (user) { + // Replace `serverSideTrackEvent` with your actual event tracking function + await serverSideTrackEvent(user.id, `Trial end email opened`); + console.log('Trial end email opened on user with email', user.email); + } else if (error) { + console.error('Error fetching user:', error); + } else { + console.log('No user found for:', payload.Recipient); + } + } + + // Respond to Postmark to acknowledge receipt of the webhook + res.status(200).json({ message: 'Webhook received' }); +} \ No newline at end of file diff --git a/pages/api/webhooks/sendgrid.ts b/pages/api/webhooks/sendgrid.ts deleted file mode 100644 index 7621e84435..0000000000 --- a/pages/api/webhooks/sendgrid.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next'; - -import { serverSideTrackEvent } from '@/utils/app/eventTracking'; -import { getAdminSupabaseClient } from '@/utils/server/supabase'; - -import { EventWebhook } from '@sendgrid/eventwebhook'; - -type SendGridPayload = { - email: string; - timestamp: number; - 'smtp-id': string; - event: string; - category: string[]; - sg_event_id: string; - sg_message_id: string; - sg_template_id: string; -}; - -const handler = async (req: NextApiRequest, res: NextApiResponse) => { - if (req.method !== 'POST') { - res.status(405).end('Method Not Allowed'); - } - - const publicKey = process.env.SENDGRID_WEBHOOK_PUBLIC_KEY || ''; - const signature = req.headers[ - 'x-twilio-email-event-webhook-signature' - ] as string; - const timestamp = req.headers[ - 'x-twilio-email-event-webhook-timestamp' - ] as string; - const payload = req.body; - - const eventWebhook = new EventWebhook(); - const key = eventWebhook.convertPublicKeyToECDSA(publicKey); - const isValidWebHookEvent = eventWebhook.verifySignature( - key, - payload, - signature, - timestamp, - ); - - if (!isValidWebHookEvent) { - return res.status(400).send(`Webhook signature verification failed.`); - } - - const eventPayload = JSON.parse(payload) as SendGridPayload[]; - const openedEvents = eventPayload.filter( - (event) => - event.event === 'open' && - event.sg_template_id === 'd-e5fff0aa9b5948b4871c436812392134', - ); - - for (const event of openedEvents) { - const supabase = getAdminSupabaseClient(); - - const { data: user } = await supabase - .from('profiles') - .select('id, email') - .eq('email', event.email) - .single(); - - if (user) { - await serverSideTrackEvent(user.id, `Trial end email opened`); - console.log('Trial end email opened on user with email', user.email); - } - } - - return res.status(200); -}; - -export default handler;