diff --git a/apps/web/app/(app)/admin/AdminUpgradeUserForm.tsx b/apps/web/app/(app)/admin/AdminUpgradeUserForm.tsx
index 1a1509805e..b605e22f56 100644
--- a/apps/web/app/(app)/admin/AdminUpgradeUserForm.tsx
+++ b/apps/web/app/(app)/admin/AdminUpgradeUserForm.tsx
@@ -90,6 +90,14 @@ export const AdminUpgradeUserForm = () => {
label: PremiumTier.PRO_MONTHLY,
value: PremiumTier.PRO_MONTHLY,
},
+ {
+ label: PremiumTier.BASIC_ANNUALLY,
+ value: PremiumTier.BASIC_ANNUALLY,
+ },
+ {
+ label: PremiumTier.BASIC_MONTHLY,
+ value: PremiumTier.BASIC_MONTHLY,
+ },
{
label: PremiumTier.LIFETIME,
value: PremiumTier.LIFETIME,
diff --git a/apps/web/app/(app)/admin/validation.tsx b/apps/web/app/(app)/admin/validation.tsx
index d25a823bcd..f7ddbb11fc 100644
--- a/apps/web/app/(app)/admin/validation.tsx
+++ b/apps/web/app/(app)/admin/validation.tsx
@@ -12,6 +12,8 @@ export const changePremiumStatusSchema = z.object({
.optional()
.transform((v) => v || undefined),
period: z.enum([
+ PremiumTier.BASIC_MONTHLY,
+ PremiumTier.BASIC_ANNUALLY,
PremiumTier.PRO_MONTHLY,
PremiumTier.PRO_ANNUALLY,
PremiumTier.BUSINESS_MONTHLY,
diff --git a/apps/web/app/(app)/premium/Pricing.tsx b/apps/web/app/(app)/premium/Pricing.tsx
index a43243d2b3..db55335181 100644
--- a/apps/web/app/(app)/premium/Pricing.tsx
+++ b/apps/web/app/(app)/premium/Pricing.tsx
@@ -132,7 +132,7 @@ export function Pricing() {
- SAVE up to 36%!
+ Save up to 40%!
@@ -146,13 +146,11 @@ export function Pricing() {
? isCurrentPlan
? "#"
: buildLemonUrl(
- tier.checkout
- ? attachUserInfo(tier.href[frequency.value], {
- id: user.id,
- email: user.email!,
- name: user.name,
- })
- : tier.href[frequency.value],
+ attachUserInfo(tier.href[frequency.value], {
+ id: user.id,
+ email: user.email!,
+ name: user.name,
+ }),
affiliateCode,
)
: "/login?next=/premium";
@@ -195,7 +193,9 @@ export function Pricing() {
{!!tier.discount?.[frequency.value] && (
- SAVE {tier.discount[frequency.value].toFixed(0)}%
+
+ SAVE {tier.discount[frequency.value].toFixed(0)}%
+
)}
@@ -356,7 +356,7 @@ function LifetimePricing(props: {
function Badge({ children }: { children: React.ReactNode }) {
return (
-
+
{children}
);
diff --git a/apps/web/app/(app)/premium/config.ts b/apps/web/app/(app)/premium/config.ts
index b9bc3f1dcd..ce03b1d9f5 100644
--- a/apps/web/app/(app)/premium/config.ts
+++ b/apps/web/app/(app)/premium/config.ts
@@ -7,6 +7,8 @@ export const frequencies = [
];
export const pricing: Record = {
+ [PremiumTier.BASIC_MONTHLY]: 10,
+ [PremiumTier.BASIC_ANNUALLY]: 6,
[PremiumTier.PRO_MONTHLY]: 14,
[PremiumTier.PRO_ANNUALLY]: 9,
[PremiumTier.BUSINESS_MONTHLY]: 22,
@@ -15,6 +17,8 @@ export const pricing: Record = {
};
export const pricingAdditonalEmail: Record = {
+ [PremiumTier.BASIC_MONTHLY]: 2,
+ [PremiumTier.BASIC_ANNUALLY]: 1.5,
[PremiumTier.PRO_MONTHLY]: 3,
[PremiumTier.PRO_ANNUALLY]: 2.5,
[PremiumTier.BUSINESS_MONTHLY]: 3.5,
@@ -28,16 +32,32 @@ function discount(monthly: number, annually: number) {
export const tiers = [
{
- name: "Free",
- href: { monthly: "/welcome", annually: "/welcome" },
- price: { monthly: 0, annually: 0 },
- description: "Try Inbox Zero for free.",
+ name: "Basic",
+ tiers: {
+ monthly: PremiumTier.BASIC_MONTHLY,
+ annually: PremiumTier.BASIC_ANNUALLY,
+ },
+ href: {
+ monthly: env.NEXT_PUBLIC_BASIC_MONTHLY_PAYMENT_LINK,
+ annually: env.NEXT_PUBLIC_BASIC_ANNUALLY_PAYMENT_LINK,
+ },
+ price: { monthly: pricing.BASIC_MONTHLY, annually: pricing.BASIC_ANNUALLY },
+ priceAdditional: {
+ monthly: pricingAdditonalEmail.BASIC_MONTHLY,
+ annually: pricingAdditonalEmail.BASIC_ANNUALLY,
+ },
+ discount: {
+ monthly: 0,
+ annually: discount(pricing.BASIC_MONTHLY, pricing.BASIC_ANNUALLY),
+ },
+ description: "Unlimited unsubscribe credits.",
features: [
"Bulk email unsubscriber",
- `Unsubscribe from ${env.NEXT_PUBLIC_UNSUBSCRIBE_CREDITS} emails per month`,
+ "Unlimited unsubscribes",
+ "Unlimited archives",
"Email analytics",
],
- cta: "Get Started",
+ cta: "Upgrade",
},
{
name: "Pro",
@@ -49,7 +69,6 @@ export const tiers = [
monthly: env.NEXT_PUBLIC_PRO_MONTHLY_PAYMENT_LINK,
annually: env.NEXT_PUBLIC_PRO_ANNUALLY_PAYMENT_LINK,
},
- checkout: true,
price: { monthly: pricing.PRO_MONTHLY, annually: pricing.PRO_ANNUALLY },
priceAdditional: {
monthly: pricingAdditonalEmail.PRO_MONTHLY,
@@ -59,11 +78,9 @@ export const tiers = [
monthly: 0,
annually: discount(pricing.PRO_MONTHLY, pricing.PRO_ANNUALLY),
},
- description:
- "Unlimited unsubscribe credits. Unlock AI features when using your own OpenAI key",
+ description: "Unlock AI features when using your own OpenAI key",
features: [
"Everything in free",
- "Unlimited unsubscribes",
"AI automation when using your own OpenAI API key",
"Cold email blocker when using your own OpenAI API key",
],
@@ -80,7 +97,6 @@ export const tiers = [
monthly: env.NEXT_PUBLIC_BUSINESS_MONTHLY_PAYMENT_LINK,
annually: env.NEXT_PUBLIC_BUSINESS_ANNUALLY_PAYMENT_LINK,
},
- checkout: true,
price: {
monthly: pricing.BUSINESS_MONTHLY,
annually: pricing.BUSINESS_ANNUALLY,
@@ -96,9 +112,7 @@ export const tiers = [
description: "Unlock full AI-powered email management",
features: [
"Everything in pro",
- "AI automation",
- "Cold email blocker",
- "AI categorization",
+ "Unlimited AI credits",
"No need to provide your own OpenAI API key",
"Priority support",
],
diff --git a/apps/web/app/(app)/usage/usage.tsx b/apps/web/app/(app)/usage/usage.tsx
index 7201feaddc..14ec88e349 100644
--- a/apps/web/app/(app)/usage/usage.tsx
+++ b/apps/web/app/(app)/usage/usage.tsx
@@ -26,7 +26,7 @@ export function Usage(props: {
? "Unlimited"
: formatStat(
data?.premium?.unsubscribeCredits ??
- env.NEXT_PUBLIC_UNSUBSCRIBE_CREDITS,
+ env.NEXT_PUBLIC_FREE_UNSUBSCRIBE_CREDITS,
),
subvalue: "credits",
icon: ,
diff --git a/apps/web/app/api/user/me/route.ts b/apps/web/app/api/user/me/route.ts
index f22714bfae..0edc2756ca 100644
--- a/apps/web/app/api/user/me/route.ts
+++ b/apps/web/app/api/user/me/route.ts
@@ -23,6 +23,7 @@ async function getUser(userId: string) {
lemonSqueezySubscriptionId: true,
lemonSqueezyRenewsAt: true,
unsubscribeCredits: true,
+ bulkUnsubscribeAccess: true,
aiAutomationAccess: true,
coldEmailBlockerAccess: true,
tier: true,
diff --git a/apps/web/components/PremiumAlert.tsx b/apps/web/components/PremiumAlert.tsx
index cef6cfb813..0afff31e1e 100644
--- a/apps/web/components/PremiumAlert.tsx
+++ b/apps/web/components/PremiumAlert.tsx
@@ -18,30 +18,31 @@ import { PremiumTier } from "@prisma/client";
export function usePremium() {
const swrResponse = useSWR("/api/user/me");
+ const { data } = swrResponse;
- const premium = !!(
- swrResponse.data?.premium &&
- isPremium(swrResponse.data.premium.lemonSqueezyRenewsAt)
- );
+ const premium = data?.premium;
+ const openAIApiKey = data?.openAIApiKey;
+
+ const isUserPremium = !!(premium && isPremium(premium.lemonSqueezyRenewsAt));
const isProPlanWithoutApiKey =
- (swrResponse.data?.premium?.tier === PremiumTier.PRO_MONTHLY ||
- swrResponse.data?.premium?.tier === PremiumTier.PRO_ANNUALLY) &&
- !swrResponse.data?.openAIApiKey;
+ (premium?.tier === PremiumTier.PRO_MONTHLY ||
+ premium?.tier === PremiumTier.PRO_ANNUALLY) &&
+ !openAIApiKey;
return {
...swrResponse,
- isPremium: premium,
+ isPremium: isUserPremium,
hasUnsubscribeAccess:
- premium ||
- hasUnsubscribeAccess(swrResponse.data?.premium?.unsubscribeCredits),
- hasAiAccess: hasAiAccess(
- swrResponse.data?.premium?.aiAutomationAccess,
- swrResponse.data?.openAIApiKey,
- ),
+ isUserPremium ||
+ hasUnsubscribeAccess(
+ premium?.bulkUnsubscribeAccess,
+ premium?.unsubscribeCredits,
+ ),
+ hasAiAccess: hasAiAccess(premium?.aiAutomationAccess, openAIApiKey),
hasColdEmailAccess: hasColdEmailAccess(
- swrResponse.data?.premium?.coldEmailBlockerAccess,
- swrResponse.data?.openAIApiKey,
+ premium?.coldEmailBlockerAccess,
+ openAIApiKey,
),
isProPlanWithoutApiKey,
};
diff --git a/apps/web/env.mjs b/apps/web/env.mjs
index 09c366bd34..32105413b0 100644
--- a/apps/web/env.mjs
+++ b/apps/web/env.mjs
@@ -40,6 +40,11 @@ export const env = createEnv({
NEXT_PUBLIC_LEMON_STORE_ID: z.string().nullish().default("inboxzero"),
// lemon plans
+ // basic
+ NEXT_PUBLIC_BASIC_MONTHLY_PAYMENT_LINK: z.string(),
+ NEXT_PUBLIC_BASIC_ANNUALLY_PAYMENT_LINK: z.string(),
+ NEXT_PUBLIC_BASIC_MONTHLY_VARIANT_ID: z.coerce.number().default(0),
+ NEXT_PUBLIC_BASIC_ANNUALLY_VARIANT_ID: z.coerce.number().default(0),
// pro
NEXT_PUBLIC_PRO_MONTHLY_PAYMENT_LINK: z.string().default(""),
NEXT_PUBLIC_PRO_ANNUALLY_PAYMENT_LINK: z.string().default(""),
@@ -63,7 +68,7 @@ export const env = createEnv({
LICENSE_10_SEAT_VARIANT_ID: z.coerce.number().optional(),
LICENSE_25_SEAT_VARIANT_ID: z.coerce.number().optional(),
- NEXT_PUBLIC_UNSUBSCRIBE_CREDITS: z.number().default(5),
+ NEXT_PUBLIC_FREE_UNSUBSCRIBE_CREDITS: z.number().default(5),
NEXT_PUBLIC_CALL_LINK: z
.string()
.default("https://cal.com/team/inbox-zero/feedback"),
@@ -85,6 +90,15 @@ export const env = createEnv({
experimental__runtimeEnv: {
NEXT_PUBLIC_LEMON_STORE_ID: process.env.NEXT_PUBLIC_LEMON_STORE_ID,
+ // basic
+ NEXT_PUBLIC_BASIC_MONTHLY_PAYMENT_LINK:
+ process.env.NEXT_PUBLIC_BASIC_MONTHLY_PAYMENT_LINK,
+ NEXT_PUBLIC_BASIC_ANNUALLY_PAYMENT_LINK:
+ process.env.NEXT_PUBLIC_BASIC_ANNUALLY_PAYMENT_LINK,
+ NEXT_PUBLIC_BASIC_MONTHLY_VARIANT_ID:
+ process.env.NEXT_PUBLIC_BASIC_MONTHLY_VARIANT_ID,
+ NEXT_PUBLIC_BASIC_ANNUALLY_VARIANT_ID:
+ process.env.NEXT_PUBLIC_BASIC_ANNUALLY_VARIANT_ID,
// pro
NEXT_PUBLIC_PRO_MONTHLY_PAYMENT_LINK:
process.env.NEXT_PUBLIC_PRO_MONTHLY_PAYMENT_LINK,
@@ -121,8 +135,8 @@ export const env = createEnv({
NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL,
NEXT_PUBLIC_CONTACTS_ENABLED: process.env.NEXT_PUBLIC_CONTACTS_ENABLED,
NEXT_PUBLIC_LEMON_STORE_ID: process.env.NEXT_PUBLIC_LEMON_STORE_ID,
- NEXT_PUBLIC_UNSUBSCRIBE_CREDITS:
- process.env.NEXT_PUBLIC_UNSUBSCRIBE_CREDITS,
+ NEXT_PUBLIC_FREE_UNSUBSCRIBE_CREDITS:
+ process.env.NEXT_PUBLIC_FREE_UNSUBSCRIBE_CREDITS,
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
NEXT_PUBLIC_SUPPORT_EMAIL: process.env.NEXT_PUBLIC_SUPPORT_EMAIL,
NEXT_PUBLIC_GTM_ID: process.env.NEXT_PUBLIC_GTM_ID,
diff --git a/apps/web/prisma/migrations/20240528181840_premium_basic/migration.sql b/apps/web/prisma/migrations/20240528181840_premium_basic/migration.sql
new file mode 100644
index 0000000000..94b91229f6
--- /dev/null
+++ b/apps/web/prisma/migrations/20240528181840_premium_basic/migration.sql
@@ -0,0 +1,13 @@
+-- AlterEnum
+-- This migration adds more than one value to an enum.
+-- With PostgreSQL versions 11 and earlier, this is not possible
+-- in a single migration. This can be worked around by creating
+-- multiple migrations, each migration adding only one value to
+-- the enum.
+
+
+ALTER TYPE "PremiumTier" ADD VALUE 'BASIC_MONTHLY';
+ALTER TYPE "PremiumTier" ADD VALUE 'BASIC_ANNUALLY';
+
+-- AlterTable
+ALTER TABLE "Premium" ADD COLUMN "bulkUnsubscribeAccess" "FeatureAccess";
diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma
index 0315cd305c..a3835c772f 100644
--- a/apps/web/prisma/schema.prisma
+++ b/apps/web/prisma/schema.prisma
@@ -109,9 +109,10 @@ model Premium {
tier PremiumTier?
// feature access
+ bulkUnsubscribeAccess FeatureAccess?
coldEmailBlockerAccess FeatureAccess?
aiAutomationAccess FeatureAccess?
- emailAccountsAccess Int? // only used for lifetime
+ emailAccountsAccess Int?
// unsubscribe/ai credits
// if `unsubscribeMonth` not set to this month, set to current month
@@ -342,6 +343,8 @@ enum ColdEmailSetting {
}
enum PremiumTier {
+ BASIC_MONTHLY
+ BASIC_ANNUALLY
PRO_MONTHLY
PRO_ANNUALLY
BUSINESS_MONTHLY
diff --git a/apps/web/utils/actions/premium.ts b/apps/web/utils/actions/premium.ts
index 53133b074a..4b65165eb1 100644
--- a/apps/web/utils/actions/premium.ts
+++ b/apps/web/utils/actions/premium.ts
@@ -51,7 +51,7 @@ export async function decrementUnsubscribeCredit() {
where: { id: premium.id },
data: {
// reset and use a credit
- unsubscribeCredits: env.NEXT_PUBLIC_UNSUBSCRIBE_CREDITS - 1,
+ unsubscribeCredits: env.NEXT_PUBLIC_FREE_UNSUBSCRIBE_CREDITS - 1,
unsubscribeMonth: currentMonth,
},
});
diff --git a/apps/web/utils/premium/index.ts b/apps/web/utils/premium/index.ts
index 41d4adcb54..636075534e 100644
--- a/apps/web/utils/premium/index.ts
+++ b/apps/web/utils/premium/index.ts
@@ -42,13 +42,21 @@ export const isAdminForPremium = (
};
export const hasUnsubscribeAccess = (
+ bulkUnsubscribeAccess?: FeatureAccess | null,
unsubscribeCredits?: number | null,
): boolean => {
+ if (
+ bulkUnsubscribeAccess === FeatureAccess.UNLOCKED ||
+ bulkUnsubscribeAccess === FeatureAccess.UNLOCKED_WITH_API_KEY
+ ) {
+ return true;
+ }
+
return unsubscribeCredits !== 0;
};
export const hasAiAccess = (
- aiAutomationAccess?: Premium["aiAutomationAccess"],
+ aiAutomationAccess?: FeatureAccess | null,
openAIApiKey?: string | null,
) => {
const hasAiAccess = !!(
@@ -60,7 +68,7 @@ export const hasAiAccess = (
};
export const hasColdEmailAccess = (
- coldEmailBlockerAccess?: Premium["coldEmailBlockerAccess"],
+ coldEmailBlockerAccess?: FeatureAccess | null,
openAIApiKey?: string | null,
) => {
const hasColdEmailAccess = !!(
@@ -77,11 +85,13 @@ export function isOnHigherTier(
tier2?: PremiumTier | null,
) {
const tierRanking = {
- [PremiumTier.PRO_MONTHLY]: 1,
- [PremiumTier.PRO_ANNUALLY]: 2,
- [PremiumTier.BUSINESS_MONTHLY]: 3,
- [PremiumTier.BUSINESS_ANNUALLY]: 4,
- [PremiumTier.LIFETIME]: 5,
+ [PremiumTier.BASIC_MONTHLY]: 1,
+ [PremiumTier.BASIC_ANNUALLY]: 2,
+ [PremiumTier.PRO_MONTHLY]: 3,
+ [PremiumTier.PRO_ANNUALLY]: 4,
+ [PremiumTier.BUSINESS_MONTHLY]: 5,
+ [PremiumTier.BUSINESS_ANNUALLY]: 6,
+ [PremiumTier.LIFETIME]: 7,
};
const tier1Rank = tier1 ? tierRanking[tier1] : 0;
diff --git a/apps/web/utils/premium/server.ts b/apps/web/utils/premium/server.ts
index 30762e0215..c6e321bcba 100644
--- a/apps/web/utils/premium/server.ts
+++ b/apps/web/utils/premium/server.ts
@@ -29,13 +29,10 @@ export async function upgradeToPremium(options: {
select: { premiumId: true },
});
- const accessLevel = getAccessLevelFromTier(options.tier);
-
const data = {
...rest,
lemonSqueezyRenewsAt,
- aiAutomationAccess: accessLevel,
- coldEmailBlockerAccess: accessLevel,
+ ...getTierAccess(options.tier),
};
if (user.premiumId) {
@@ -81,6 +78,7 @@ export async function cancelPremium(options: {
where: { id: options.premiumId },
data: {
lemonSqueezyRenewsAt: options.lemonSqueezyEndsAt,
+ bulkUnsubscribeAccess: null,
aiAutomationAccess: null,
coldEmailBlockerAccess: null,
},
@@ -113,15 +111,30 @@ export async function editEmailAccountsAccess(options: {
});
}
-function getAccessLevelFromTier(tier: PremiumTier): FeatureAccess {
+function getTierAccess(tier: PremiumTier) {
switch (tier) {
+ case PremiumTier.BASIC_MONTHLY:
+ case PremiumTier.BASIC_ANNUALLY:
+ return {
+ bulkUnsubscribeAccess: FeatureAccess.UNLOCKED,
+ aiAutomationAccess: FeatureAccess.LOCKED,
+ coldEmailBlockerAccess: FeatureAccess.LOCKED,
+ };
case PremiumTier.PRO_MONTHLY:
case PremiumTier.PRO_ANNUALLY:
- return FeatureAccess.UNLOCKED_WITH_API_KEY;
+ return {
+ bulkUnsubscribeAccess: FeatureAccess.UNLOCKED,
+ aiAutomationAccess: FeatureAccess.UNLOCKED_WITH_API_KEY,
+ coldEmailBlockerAccess: FeatureAccess.UNLOCKED_WITH_API_KEY,
+ };
case PremiumTier.BUSINESS_MONTHLY:
case PremiumTier.BUSINESS_ANNUALLY:
case PremiumTier.LIFETIME:
- return FeatureAccess.UNLOCKED;
+ return {
+ bulkUnsubscribeAccess: FeatureAccess.UNLOCKED,
+ aiAutomationAccess: FeatureAccess.UNLOCKED,
+ coldEmailBlockerAccess: FeatureAccess.UNLOCKED,
+ };
default:
throw new Error(`Unknown premium tier: ${tier}`);
}
diff --git a/turbo.json b/turbo.json
index df49c164fd..84fa51bebf 100644
--- a/turbo.json
+++ b/turbo.json
@@ -34,7 +34,7 @@
"NEXT_PUBLIC_LIFETIME_PAYMENT_LINK",
"NEXT_PUBLIC_LIFETIME_VARIANT_ID",
- "NEXT_PUBLIC_UNSUBSCRIBE_CREDITS",
+ "NEXT_PUBLIC_FREE_UNSUBSCRIBE_CREDITS",
"NEXT_PUBLIC_CALL_LINK",
"NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_CONTACTS_ENABLED",