diff --git a/apps/web/prisma/migrations/20250112081255_pending_invite/migration.sql b/apps/web/prisma/migrations/20250112081255_pending_invite/migration.sql new file mode 100644 index 0000000000..722e10ca50 --- /dev/null +++ b/apps/web/prisma/migrations/20250112081255_pending_invite/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Premium" ADD COLUMN "pendingInvites" TEXT[]; + +-- CreateIndex +CREATE INDEX "Premium_pendingInvites_idx" ON "Premium"("pendingInvites"); diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma index b58dcd2497..a8d55a9e4f 100644 --- a/apps/web/prisma/schema.prisma +++ b/apps/web/prisma/schema.prisma @@ -104,6 +104,8 @@ model Premium { users User[] @relation(name: "userPremium") admins User[] + pendingInvites String[] + // lemon squeezy lemonSqueezyRenewsAt DateTime? lemonSqueezyCustomerId Int? @@ -130,6 +132,8 @@ model Premium { unsubscribeCredits Int? aiMonth Int? // 1-12 aiCredits Int? + + @@index([pendingInvites]) } // not in use as it's only used for passwordless login diff --git a/apps/web/utils/actions/premium.ts b/apps/web/utils/actions/premium.ts index 2b5c1d71ad..5f2620b303 100644 --- a/apps/web/utils/actions/premium.ts +++ b/apps/web/utils/actions/premium.ts @@ -106,6 +106,7 @@ export async function updateMultiAccountPremiumAction( lemonSqueezySubscriptionItemId: true, emailAccountsAccess: true, admins: { select: { id: true } }, + pendingInvites: true, }, }, }, @@ -120,16 +121,16 @@ export async function updateMultiAccountPremiumAction( const uniqueEmails = uniq(emails); const users = await prisma.user.findMany({ where: { email: { in: uniqueEmails } }, - select: { id: true, premium: true }, + select: { id: true, premium: true, email: true }, }); const premium = user.premium || (await createPremiumForUser(session.user.id)); - const otherUsersToAdd = users.filter((u) => u.id !== session.user.id); + const otherUsers = users.filter((u) => u.id !== session.user.id); // make sure that the users being added to this plan are not on higher tiers already - for (const userToAdd of otherUsersToAdd) { + for (const userToAdd of otherUsers) { if (isOnHigherTier(userToAdd.premium?.tier, premium.tier)) { return { error: @@ -138,7 +139,7 @@ export async function updateMultiAccountPremiumAction( } } - if ((premium.emailAccountsAccess || 0) < users.length) { + if ((premium.emailAccountsAccess || 0) < uniqueEmails.length) { // TODO lifetime users if (!premium.lemonSqueezySubscriptionItemId) { return { @@ -148,7 +149,7 @@ export async function updateMultiAccountPremiumAction( await updateSubscriptionItemQuantity({ id: premium.lemonSqueezySubscriptionItemId, - quantity: otherUsersToAdd.length + 1, + quantity: uniqueEmails.length, }); } @@ -157,7 +158,7 @@ export async function updateMultiAccountPremiumAction( await prisma.premium.deleteMany({ where: { id: { not: premium.id }, - users: { some: { id: { in: otherUsersToAdd.map((u) => u.id) } } }, + users: { some: { id: { in: otherUsers.map((u) => u.id) } } }, }, }); @@ -165,20 +166,52 @@ export async function updateMultiAccountPremiumAction( await prisma.premium.update({ where: { id: premium.id }, data: { - users: { connect: otherUsersToAdd.map((user) => ({ id: user.id })) }, + users: { connect: otherUsers.map((user) => ({ id: user.id })) }, }, }); - if (users.length < uniqueEmails.length) { - return { - warning: - "Not all users exist. Each account must sign up to Inbox Zero to share premium with it.", - }; - } + // add users to pending invites + const nonExistingUsers = uniqueEmails.filter( + (email) => !users.some((u) => u.email === email), + ); + await prisma.premium.update({ + where: { id: premium.id }, + data: { + pendingInvites: { + set: uniq([...(premium.pendingInvites || []), ...nonExistingUsers]), + }, + }, + }); }, ); } +export async function handlePendingPremiumInvite(user: { email: string }) { + // Check for pending invite + const premium = await prisma.premium.findFirst({ + where: { pendingInvites: { has: user.email } }, + select: { + id: true, + pendingInvites: true, + lemonSqueezySubscriptionItemId: true, + _count: { select: { users: true } }, + }, + }); + + if (premium?.lemonSqueezySubscriptionItemId) { + // Add user to premium and remove from pending invites + await prisma.premium.update({ + where: { id: premium.id }, + data: { + users: { connect: { email: user.email } }, + pendingInvites: { + set: premium.pendingInvites.filter((email) => email !== user.email), + }, + }, + }); + } +} + export const switchPremiumPlanAction = withActionInstrumentation( "switchPremiumPlan", async (premiumTier: PremiumTier) => { diff --git a/apps/web/utils/auth.ts b/apps/web/utils/auth.ts index 82060e67df..cdefa0e219 100644 --- a/apps/web/utils/auth.ts +++ b/apps/web/utils/auth.ts @@ -9,6 +9,7 @@ import prisma from "@/utils/prisma"; import { env } from "@/env"; import { captureException } from "@/utils/error"; import { createScopedLogger } from "@/utils/logger"; +import { handlePendingPremiumInvite } from "@/utils/actions/premium"; const logger = createScopedLogger("auth"); @@ -150,6 +151,12 @@ export const getAuthOptions: (options?: { captureException(error, undefined, user.email); } } + + if (isNewUser && user.email) { + logger.info("Handling pending premium invite", { email: user.email }); + await handlePendingPremiumInvite({ email: user.email }); + logger.info("Added user to premium from invite", { email: user.email }); + } }, }, pages: {