Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f95f0b54d |
@@ -106,8 +106,19 @@ export function UpgradePlanModal({
|
||||
trigger: trigger,
|
||||
teamId: teamInfo?.currentTeam?.id,
|
||||
});
|
||||
|
||||
// Track upgrade click for email scheduling (with trigger info for personalization)
|
||||
if (teamInfo?.currentTeam?.id) {
|
||||
fetch(`/api/teams/${teamInfo.currentTeam.id}/billing/track-upgrade-click`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ trigger }),
|
||||
}).catch(() => {
|
||||
// Silently fail - this is just for tracking
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [open, trigger]);
|
||||
}, [open, trigger, teamInfo?.currentTeam?.id]);
|
||||
|
||||
// Track analytics event when child button is present
|
||||
const handleUpgradeClick = () => {
|
||||
|
||||
@@ -217,10 +217,21 @@ export function UpgradePlanModalWithDiscount({
|
||||
trigger: trigger,
|
||||
teamId,
|
||||
});
|
||||
|
||||
// Track upgrade click for email scheduling (with trigger info for personalization)
|
||||
if (teamId) {
|
||||
fetch(`/api/teams/${teamId}/billing/track-upgrade-click`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ trigger }),
|
||||
}).catch(() => {
|
||||
// Silently fail - this is just for tracking
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setDataRoomsPlanSelection("base");
|
||||
}
|
||||
}, [open, trigger]);
|
||||
}, [open, trigger, teamId]);
|
||||
|
||||
const handleUpgradeClick = () => {
|
||||
analytics.capture("Upgrade Button Clicked", {
|
||||
|
||||
@@ -193,10 +193,21 @@ export function UpgradePlanModal({
|
||||
trigger: trigger,
|
||||
teamId,
|
||||
});
|
||||
|
||||
// Track upgrade click for email scheduling (with trigger info for personalization)
|
||||
if (teamId) {
|
||||
fetch(`/api/teams/${teamId}/billing/track-upgrade-click`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ trigger }),
|
||||
}).catch(() => {
|
||||
// Silently fail - this is just for tracking
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setDataRoomsPlanSelection("base");
|
||||
}
|
||||
}, [open, trigger]);
|
||||
}, [open, trigger, teamId]);
|
||||
|
||||
const handleUpgradeClick = () => {
|
||||
analytics.capture("Upgrade Button Clicked", {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
Body,
|
||||
Head,
|
||||
Html,
|
||||
Link,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
|
||||
interface AbandonedCheckoutEmailProps {
|
||||
name: string | null | undefined;
|
||||
}
|
||||
|
||||
const AbandonedCheckoutEmail = ({ name }: AbandonedCheckoutEmailProps) => {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Tailwind>
|
||||
<Body className="font-sans text-sm">
|
||||
<Text>Hi{name && ` ${name}`},</Text>
|
||||
<Text>
|
||||
I noticed you started the checkout process but didn't complete
|
||||
it. Did something go wrong?
|
||||
</Text>
|
||||
<Text>
|
||||
If you ran into any issues or have questions about our plans,
|
||||
I'd be happy to help. Just reply to this email.
|
||||
</Text>
|
||||
<Text>
|
||||
<Link
|
||||
href="https://app.papermark.com/settings/upgrade"
|
||||
target="_blank"
|
||||
className="text-blue-500 underline"
|
||||
>
|
||||
Complete your upgrade →
|
||||
</Link>
|
||||
</Text>
|
||||
<Text>
|
||||
Best,
|
||||
<br />
|
||||
Marc
|
||||
</Text>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default AbandonedCheckoutEmail;
|
||||
@@ -0,0 +1,118 @@
|
||||
import {
|
||||
Body,
|
||||
Head,
|
||||
Html,
|
||||
Link,
|
||||
Tailwind,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
|
||||
// Map trigger names to feature titles
|
||||
const FEATURE_NAMES: Record<string, string> = {
|
||||
// Custom domains
|
||||
add_domain_overview: "Custom Domains",
|
||||
add_domain_link_sheet: "Custom Domains",
|
||||
// Data rooms
|
||||
datarooms: "Secure Data Rooms",
|
||||
add_dataroom_overview: "Secure Data Rooms",
|
||||
datarooms_generate_index_button: "Data Room Index",
|
||||
datarooms_rebuild_index_button: "Data Room Index",
|
||||
// Team features
|
||||
invite_team_members: "Team Collaboration",
|
||||
add_new_team: "Multiple Teams",
|
||||
// Visitor analytics
|
||||
"visitor-table-user-agent": "Visitor Analytics",
|
||||
// Tags
|
||||
create_tag: "Document Tags",
|
||||
// Folders
|
||||
add_folder_button: "Folders",
|
||||
// Document limits
|
||||
limit_upload_documents: "Unlimited Documents",
|
||||
limit_upload_document_version: "Document Versions",
|
||||
// Link limits
|
||||
limit_add_link: "Unlimited Links",
|
||||
// Analytics exports
|
||||
dashboard_visitors_export: "Analytics Export",
|
||||
dashboard_views_export: "Analytics Export",
|
||||
dashboard_links_export: "Analytics Export",
|
||||
dashboard_documents_export: "Analytics Export",
|
||||
dashboard_time_range_custom_select: "Custom Date Ranges",
|
||||
// Branding
|
||||
pro_banner: "Custom Branding",
|
||||
// Web links
|
||||
add_web_link_document: "Web Links",
|
||||
// Groups
|
||||
add_group_link: "Link Groups",
|
||||
};
|
||||
|
||||
// Get unique feature names from triggers (deduplicated, lowercase)
|
||||
function getUniqueFeatureNames(triggers: string[]): string[] {
|
||||
const seenTitles = new Set<string>();
|
||||
const featureNames: string[] = [];
|
||||
|
||||
for (const trigger of triggers) {
|
||||
const title = FEATURE_NAMES[trigger];
|
||||
if (title && !seenTitles.has(title)) {
|
||||
seenTitles.add(title);
|
||||
featureNames.push(title.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
return featureNames.slice(0, 3); // Max 3 features
|
||||
}
|
||||
|
||||
// Format feature names into a sentence (e.g., "secure data rooms, custom domains and team collaboration")
|
||||
function formatFeatureList(names: string[]): string {
|
||||
if (names.length === 0) return "";
|
||||
if (names.length === 1) return names[0];
|
||||
if (names.length === 2) return `${names[0]} and ${names[1]}`;
|
||||
return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
|
||||
}
|
||||
|
||||
interface UpgradeIntentEmailProps {
|
||||
name: string | null | undefined;
|
||||
triggers?: string[];
|
||||
}
|
||||
|
||||
const UpgradeIntentEmail = ({ name, triggers = [] }: UpgradeIntentEmailProps) => {
|
||||
const featureNames = getUniqueFeatureNames(triggers);
|
||||
const hasFeatures = featureNames.length > 0;
|
||||
const featureList = formatFeatureList(featureNames);
|
||||
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Tailwind>
|
||||
<Body className="font-sans text-sm">
|
||||
<Text>Hi{name && ` ${name}`},</Text>
|
||||
<Text>
|
||||
I noticed you've been exploring our upgrade options.
|
||||
{hasFeatures &&
|
||||
` I am happy to share more about Papermark ${featureList}.`}
|
||||
</Text>
|
||||
<Text>
|
||||
Is there anything holding you back or any questions I can help
|
||||
answer?
|
||||
</Text>
|
||||
<Text>
|
||||
<Link
|
||||
href="https://app.papermark.com/settings/upgrade"
|
||||
target="_blank"
|
||||
className="text-blue-500 underline"
|
||||
>
|
||||
View upgrade options →
|
||||
</Link>
|
||||
</Text>
|
||||
<Text>Just reply to this email and I'll get back to you.</Text>
|
||||
<Text>
|
||||
Best,
|
||||
<br />
|
||||
Marc
|
||||
</Text>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpgradeIntentEmail;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { sendEmail } from "@/lib/resend";
|
||||
|
||||
import AbandonedCheckoutEmail from "@/components/emails/abandoned-checkout";
|
||||
|
||||
import { CreateUserEmailProps } from "../types";
|
||||
|
||||
export const sendAbandonedCheckoutEmail = async (
|
||||
params: CreateUserEmailProps,
|
||||
) => {
|
||||
const { name, email } = params.user;
|
||||
|
||||
// Get the first name from the full name
|
||||
const firstName = name ? name.split(" ")[0] : null;
|
||||
|
||||
const emailTemplate = AbandonedCheckoutEmail({
|
||||
name: firstName,
|
||||
});
|
||||
|
||||
try {
|
||||
await sendEmail({
|
||||
to: email as string,
|
||||
subject: "Did something block checkout?",
|
||||
from: "Marc Seitz <marc@papermark.com>",
|
||||
react: emailTemplate,
|
||||
test: process.env.NODE_ENV === "development",
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { sendEmail } from "@/lib/resend";
|
||||
|
||||
import UpgradeIntentEmail from "@/components/emails/upgrade-intent";
|
||||
|
||||
interface SendUpgradeIntentEmailProps {
|
||||
user: {
|
||||
name: string | null | undefined;
|
||||
email: string | null | undefined;
|
||||
};
|
||||
triggers?: string[];
|
||||
}
|
||||
|
||||
export const sendUpgradeIntentEmail = async (
|
||||
params: SendUpgradeIntentEmailProps,
|
||||
) => {
|
||||
const { name, email } = params.user;
|
||||
const triggers = params.triggers || [];
|
||||
|
||||
// Get the first name from the full name
|
||||
const firstName = name ? name.split(" ")[0] : null;
|
||||
|
||||
const emailTemplate = UpgradeIntentEmail({
|
||||
name: firstName,
|
||||
triggers,
|
||||
});
|
||||
|
||||
try {
|
||||
await sendEmail({
|
||||
to: email as string,
|
||||
subject: "Need help with your upgrade?",
|
||||
from: "Marc Seitz <marc@papermark.com>",
|
||||
react: emailTemplate,
|
||||
test: process.env.NODE_ENV === "development",
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
@@ -1,10 +1,13 @@
|
||||
import { logger, task } from "@trigger.dev/sdk/v3";
|
||||
|
||||
import { sendAbandonedCheckoutEmail } from "@/lib/emails/send-abandoned-checkout";
|
||||
import { sendDataroomInfoEmail } from "@/lib/emails/send-dataroom-info";
|
||||
import { sendDataroomTrial24hReminderEmail } from "@/lib/emails/send-dataroom-trial-24h";
|
||||
import { sendDataroomTrialEndEmail } from "@/lib/emails/send-dataroom-trial-end";
|
||||
import { sendUpgradeIntentEmail } from "@/lib/emails/send-upgrade-intent";
|
||||
import { sendUpgradeOneMonthCheckinEmail } from "@/lib/emails/send-upgrade-month-checkin";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { redis } from "@/lib/redis";
|
||||
|
||||
export const sendDataroomTrialInfoEmailTask = task({
|
||||
id: "send-dataroom-trial-info-email",
|
||||
@@ -171,3 +174,121 @@ export const sendUpgradeOneMonthCheckinEmailTask = task({
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const sendAbandonedCheckoutEmailTask = task({
|
||||
id: "send-abandoned-checkout-email",
|
||||
retry: { maxAttempts: 3 },
|
||||
run: async (payload: { to: string; name: string; teamId: string }) => {
|
||||
try {
|
||||
const team = await prisma.team.findUnique({
|
||||
where: { id: payload.teamId },
|
||||
select: {
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
logger.error("Team not found", { teamId: payload.teamId });
|
||||
return;
|
||||
}
|
||||
|
||||
// If team already upgraded to a paid plan, don't send abandoned checkout email
|
||||
const paidPlans = [
|
||||
"pro",
|
||||
"business",
|
||||
"datarooms",
|
||||
"datarooms-plus",
|
||||
"datarooms-premium",
|
||||
];
|
||||
const isPaidPlan = paidPlans.some((plan) => team.plan.includes(plan));
|
||||
|
||||
if (isPaidPlan) {
|
||||
logger.info("Team already on paid plan - no abandoned checkout email needed", {
|
||||
teamId: payload.teamId,
|
||||
plan: team.plan,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await sendAbandonedCheckoutEmail({
|
||||
user: { email: payload.to, name: payload.name },
|
||||
});
|
||||
logger.info("Abandoned checkout email sent", { to: payload.to });
|
||||
} catch (error) {
|
||||
logger.error("Error sending abandoned checkout email", { error });
|
||||
return;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const sendUpgradeIntentEmailTask = task({
|
||||
id: "send-upgrade-intent-email",
|
||||
retry: { maxAttempts: 3 },
|
||||
run: async (payload: {
|
||||
to: string;
|
||||
name: string;
|
||||
teamId: string;
|
||||
triggers?: string[];
|
||||
}) => {
|
||||
try {
|
||||
const team = await prisma.team.findUnique({
|
||||
where: { id: payload.teamId },
|
||||
select: {
|
||||
plan: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
logger.error("Team not found", { teamId: payload.teamId });
|
||||
return;
|
||||
}
|
||||
|
||||
// If team already upgraded to a paid plan or proceeded to checkout, don't send email
|
||||
const paidPlans = [
|
||||
"pro",
|
||||
"business",
|
||||
"datarooms",
|
||||
"datarooms-plus",
|
||||
"datarooms-premium",
|
||||
];
|
||||
const isPaidPlan = paidPlans.some((plan) => team.plan.includes(plan));
|
||||
|
||||
if (isPaidPlan) {
|
||||
logger.info("Team already on paid plan - no upgrade intent email needed", {
|
||||
teamId: payload.teamId,
|
||||
plan: team.plan,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user proceeded to checkout (by checking if they have checkout session started)
|
||||
const checkoutKey = `checkout:started:${payload.teamId}`;
|
||||
const checkoutStarted = await redis.get(checkoutKey);
|
||||
|
||||
if (checkoutStarted) {
|
||||
logger.info("User already proceeded to checkout - no upgrade intent email needed", {
|
||||
teamId: payload.teamId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await sendUpgradeIntentEmail({
|
||||
user: { email: payload.to, name: payload.name },
|
||||
triggers: payload.triggers,
|
||||
});
|
||||
logger.info("Upgrade intent email sent", {
|
||||
to: payload.to,
|
||||
triggers: payload.triggers,
|
||||
});
|
||||
|
||||
// Clear the upgrade clicks and triggers after sending email
|
||||
const upgradeClicksKey = `upgrade:clicks:${payload.teamId}`;
|
||||
const upgradeTriggersKey = `upgrade:triggers:${payload.teamId}`;
|
||||
await redis.del(upgradeClicksKey);
|
||||
await redis.del(upgradeTriggersKey);
|
||||
} catch (error) {
|
||||
logger.error("Error sending upgrade intent email", { error });
|
||||
return;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { waitUntil } from "@vercel/functions";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
import { redis } from "@/lib/redis";
|
||||
import { sendUpgradeIntentEmailTask } from "@/lib/trigger/send-scheduled-email";
|
||||
import { CustomUser } from "@/lib/types";
|
||||
|
||||
import { authOptions } from "../../../auth/[...nextauth]";
|
||||
|
||||
export const config = {
|
||||
supportsResponseStreaming: true,
|
||||
};
|
||||
|
||||
const THREE_DAYS_IN_SECONDS = 3 * 24 * 60 * 60;
|
||||
const REQUIRED_CLICKS = 3;
|
||||
|
||||
export default async function handle(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
if (req.method === "POST") {
|
||||
const session = await getServerSession(req, res, authOptions);
|
||||
if (!session) {
|
||||
return res.status(401).end("Unauthorized");
|
||||
}
|
||||
|
||||
const { teamId } = req.query as { teamId: string };
|
||||
const { trigger } = req.body as { trigger?: string };
|
||||
const {
|
||||
id: userId,
|
||||
email: userEmail,
|
||||
name: userName,
|
||||
} = session.user as CustomUser;
|
||||
|
||||
// Verify user belongs to team
|
||||
const userTeam = await prisma.userTeam.findFirst({
|
||||
where: {
|
||||
teamId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!userTeam) {
|
||||
return res.status(404).end("Team not found");
|
||||
}
|
||||
|
||||
// Check if team is already on a paid plan
|
||||
const team = await prisma.team.findUnique({
|
||||
where: { id: teamId },
|
||||
select: { plan: true },
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
return res.status(404).end("Team not found");
|
||||
}
|
||||
|
||||
const paidPlans = [
|
||||
"pro",
|
||||
"business",
|
||||
"datarooms",
|
||||
"datarooms-plus",
|
||||
"datarooms-premium",
|
||||
];
|
||||
const isPaidPlan = paidPlans.some((plan) => team.plan.includes(plan));
|
||||
|
||||
if (isPaidPlan) {
|
||||
// Team already on paid plan, no need to track
|
||||
return res.status(200).json({ tracked: false, reason: "already_paid" });
|
||||
}
|
||||
|
||||
const upgradeClicksKey = `upgrade:clicks:${teamId}`;
|
||||
const upgradeTriggersKey = `upgrade:triggers:${teamId}`;
|
||||
const now = Date.now();
|
||||
const threeDaysAgo = now - THREE_DAYS_IN_SECONDS * 1000;
|
||||
|
||||
// Get existing clicks
|
||||
const existingClicks = await redis.zrange(upgradeClicksKey, 0, -1, {
|
||||
withScores: true,
|
||||
});
|
||||
|
||||
// Filter clicks within the last 3 days
|
||||
const recentClicks: number[] = [];
|
||||
for (let i = 0; i < existingClicks.length; i += 2) {
|
||||
const timestamp = existingClicks[i + 1] as number;
|
||||
if (timestamp > threeDaysAgo) {
|
||||
recentClicks.push(timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new click
|
||||
await redis.zadd(upgradeClicksKey, { score: now, member: now.toString() });
|
||||
|
||||
// Store the trigger if provided
|
||||
if (trigger) {
|
||||
await redis.sadd(upgradeTriggersKey, trigger);
|
||||
await redis.expire(upgradeTriggersKey, THREE_DAYS_IN_SECONDS);
|
||||
}
|
||||
|
||||
// Remove old clicks (older than 3 days)
|
||||
await redis.zremrangebyscore(upgradeClicksKey, 0, threeDaysAgo);
|
||||
|
||||
// Set expiry on the key (3 days from now)
|
||||
await redis.expire(upgradeClicksKey, THREE_DAYS_IN_SECONDS);
|
||||
|
||||
const totalRecentClicks = recentClicks.length + 1;
|
||||
|
||||
// If this is the 3rd click (or more) within 3 days, schedule the email
|
||||
if (totalRecentClicks >= REQUIRED_CLICKS) {
|
||||
// Check if we already scheduled an email for this session
|
||||
const emailScheduledKey = `upgrade:email-scheduled:${teamId}`;
|
||||
const alreadyScheduled = await redis.get(emailScheduledKey);
|
||||
|
||||
if (!alreadyScheduled) {
|
||||
// Get all triggers the user clicked on
|
||||
const triggers = (await redis.smembers(upgradeTriggersKey)) as string[];
|
||||
|
||||
// Mark as scheduled
|
||||
await redis.set(emailScheduledKey, "1", { ex: THREE_DAYS_IN_SECONDS });
|
||||
|
||||
// Schedule email for 1 day after the last click
|
||||
waitUntil(
|
||||
sendUpgradeIntentEmailTask.trigger(
|
||||
{
|
||||
to: userEmail as string,
|
||||
name: userName as string,
|
||||
teamId,
|
||||
triggers,
|
||||
},
|
||||
{
|
||||
delay: "1d",
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return res.status(200).json({
|
||||
tracked: true,
|
||||
clicks: totalRecentClicks,
|
||||
triggers,
|
||||
emailScheduled: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
tracked: true,
|
||||
clicks: totalRecentClicks,
|
||||
emailScheduled: false,
|
||||
});
|
||||
} else {
|
||||
res.setHeader("Allow", ["POST"]);
|
||||
return res.status(405).end(`Method ${req.method} Not Allowed`);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import { getServerSession } from "next-auth/next";
|
||||
import { identifyUser, trackAnalytics } from "@/lib/analytics";
|
||||
import { getDubDiscountForExternalUserId } from "@/lib/dub";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { redis } from "@/lib/redis";
|
||||
import { sendAbandonedCheckoutEmailTask } from "@/lib/trigger/send-scheduled-email";
|
||||
import { CustomUser } from "@/lib/types";
|
||||
import { getIpAddress } from "@/lib/utils/ip";
|
||||
|
||||
@@ -52,7 +54,7 @@ export default async function handle(
|
||||
applyYearlyDiscount?: string;
|
||||
};
|
||||
|
||||
const { id: userId, email: userEmail } = session.user as CustomUser;
|
||||
const { id: userId, email: userEmail, name: userName } = session.user as CustomUser;
|
||||
|
||||
const team = await prisma.team.findUnique({
|
||||
where: {
|
||||
@@ -164,6 +166,10 @@ export default async function handle(
|
||||
});
|
||||
}
|
||||
|
||||
// Mark that user proceeded to checkout (for upgrade intent email check)
|
||||
const checkoutStartedKey = `checkout:started:${teamId}`;
|
||||
await redis.set(checkoutStartedKey, "1", { ex: 3 * 24 * 60 * 60 }); // Expires in 3 days
|
||||
|
||||
waitUntil(
|
||||
Promise.all([
|
||||
identifyUser(userEmail ?? userId),
|
||||
@@ -175,6 +181,21 @@ export default async function handle(
|
||||
]),
|
||||
);
|
||||
|
||||
// Schedule abandoned checkout email to be sent in 1 hour
|
||||
// The task will check if the team has already upgraded before sending
|
||||
waitUntil(
|
||||
sendAbandonedCheckoutEmailTask.trigger(
|
||||
{
|
||||
to: userEmail as string,
|
||||
name: userName as string,
|
||||
teamId,
|
||||
},
|
||||
{
|
||||
delay: "1h",
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return res.status(200).json(stripeSession);
|
||||
} else {
|
||||
// We only allow POST requests
|
||||
|
||||
Reference in New Issue
Block a user