feature: onboarding emails
* wip * wip * wip * fix coderabbit comments * remove template
This commit is contained in:
committed by
GitHub
parent
67301d928c
commit
e645c094b2
@@ -3,6 +3,7 @@ import { chartRouter } from './routers/chart';
|
||||
import { chatRouter } from './routers/chat';
|
||||
import { clientRouter } from './routers/client';
|
||||
import { dashboardRouter } from './routers/dashboard';
|
||||
import { emailRouter } from './routers/email';
|
||||
import { eventRouter } from './routers/event';
|
||||
import { importRouter } from './routers/import';
|
||||
import { insightRouter } from './routers/insight';
|
||||
@@ -51,6 +52,7 @@ export const appRouter = createTRPCRouter({
|
||||
chat: chatRouter,
|
||||
insight: insightRouter,
|
||||
widget: widgetRouter,
|
||||
email: emailRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
@@ -353,7 +353,6 @@ export const authRouter = createTRPCRouter({
|
||||
.input(zSignInShare)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { password, shareId, shareType = 'overview' } = input;
|
||||
|
||||
let share: { password: string | null; public: boolean } | null = null;
|
||||
let cookieName = '';
|
||||
|
||||
|
||||
114
packages/trpc/src/routers/email.ts
Normal file
114
packages/trpc/src/routers/email.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { emailCategories } from '@openpanel/constants';
|
||||
import { db } from '@openpanel/db';
|
||||
import { verifyUnsubscribeToken } from '@openpanel/email';
|
||||
import { z } from 'zod';
|
||||
import { TRPCBadRequestError } from '../errors';
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
|
||||
|
||||
export const emailRouter = createTRPCRouter({
|
||||
unsubscribe: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
email: z.string().email(),
|
||||
category: z.string(),
|
||||
token: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const { email, category, token } = input;
|
||||
|
||||
// Verify token
|
||||
if (!verifyUnsubscribeToken(email, category, token)) {
|
||||
throw TRPCBadRequestError('Invalid unsubscribe link');
|
||||
}
|
||||
|
||||
// Upsert the unsubscribe record
|
||||
await db.emailUnsubscribe.upsert({
|
||||
where: {
|
||||
email_category: {
|
||||
email,
|
||||
category,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
email,
|
||||
category,
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
getPreferences: protectedProcedure.query(async ({ ctx }) => {
|
||||
if (!ctx.session.userId || !ctx.session.user?.email) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
|
||||
const email = ctx.session.user.email;
|
||||
|
||||
// Get all unsubscribe records for this user
|
||||
const unsubscribes = await db.emailUnsubscribe.findMany({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
select: {
|
||||
category: true,
|
||||
},
|
||||
});
|
||||
|
||||
const unsubscribedCategories = new Set(unsubscribes.map((u) => u.category));
|
||||
|
||||
// Return object with all categories, true = subscribed (not unsubscribed)
|
||||
const preferences: Record<string, boolean> = {};
|
||||
for (const [category] of Object.entries(emailCategories)) {
|
||||
preferences[category] = !unsubscribedCategories.has(category);
|
||||
}
|
||||
|
||||
return preferences;
|
||||
}),
|
||||
|
||||
updatePreferences: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
categories: z.record(z.string(), z.boolean()),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
if (!ctx.session.userId || !ctx.session.user?.email) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
|
||||
const email = ctx.session.user.email;
|
||||
|
||||
// Process each category
|
||||
for (const [category, subscribed] of Object.entries(input.categories)) {
|
||||
if (subscribed) {
|
||||
// User wants to subscribe - delete unsubscribe record if exists
|
||||
await db.emailUnsubscribe.deleteMany({
|
||||
where: {
|
||||
email,
|
||||
category,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// User wants to unsubscribe - upsert unsubscribe record
|
||||
await db.emailUnsubscribe.upsert({
|
||||
where: {
|
||||
email_category: {
|
||||
email,
|
||||
category,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
email,
|
||||
category,
|
||||
},
|
||||
update: {},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
});
|
||||
@@ -8,7 +8,6 @@ import { zOnboardingProject } from '@openpanel/validation';
|
||||
|
||||
import { hashPassword } from '@openpanel/common/server';
|
||||
import { addDays } from 'date-fns';
|
||||
import { addTrialEndingSoonJob, miscQueue } from '../../../queue';
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
|
||||
|
||||
async function createOrGetOrganization(
|
||||
@@ -30,16 +29,10 @@ async function createOrGetOrganization(
|
||||
subscriptionEndsAt: addDays(new Date(), TRIAL_DURATION_IN_DAYS),
|
||||
subscriptionStatus: 'trialing',
|
||||
timezone: input.timezone,
|
||||
onboarding: '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!process.env.SELF_HOSTED) {
|
||||
await addTrialEndingSoonJob(
|
||||
organization.id,
|
||||
1000 * 60 * 60 * 24 * TRIAL_DURATION_IN_DAYS * 0.9,
|
||||
);
|
||||
}
|
||||
|
||||
return organization;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user