Skip to content

Commit

Permalink
add bulk unsubscribe tier
Browse files Browse the repository at this point in the history
  • Loading branch information
elie222 committed May 28, 2024
1 parent 36c8f87 commit a2f0254
Show file tree
Hide file tree
Showing 11 changed files with 99 additions and 52 deletions.
8 changes: 5 additions & 3 deletions apps/web/app/(app)/premium/Pricing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export function Pricing() {
</RadioGroup>

<div className="ml-1">
<Badge>SAVE up to 36%!</Badge>
<Badge>Save up to 40%!</Badge>
</div>
</div>

Expand Down Expand Up @@ -195,7 +195,9 @@ export function Pricing() {

{!!tier.discount?.[frequency.value] && (
<Badge>
SAVE {tier.discount[frequency.value].toFixed(0)}%
<span className="tracking-wide">
SAVE {tier.discount[frequency.value].toFixed(0)}%
</span>
</Badge>
)}
</p>
Expand Down Expand Up @@ -356,7 +358,7 @@ function LifetimePricing(props: {

function Badge({ children }: { children: React.ReactNode }) {
return (
<p className="rounded-full bg-blue-600/10 px-2.5 py-1 text-xs font-bold leading-5 text-blue-600">
<p className="rounded-full bg-blue-600/10 px-2.5 py-1 text-xs font-semibold leading-5 text-blue-600">
{children}
</p>
);
Expand Down
36 changes: 24 additions & 12 deletions apps/web/app/(app)/premium/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export const frequencies = [
];

export const pricing: Record<PremiumTier, number> = {
[PremiumTier.BASIC_MONTHLY]: 10,
[PremiumTier.BASIC_ANNUALLY]: 6,
[PremiumTier.PRO_MONTHLY]: 14,
[PremiumTier.PRO_ANNUALLY]: 9,
[PremiumTier.BUSINESS_MONTHLY]: 22,
Expand All @@ -15,6 +17,8 @@ export const pricing: Record<PremiumTier, number> = {
};

export const pricingAdditonalEmail: Record<PremiumTier, number> = {
[PremiumTier.BASIC_MONTHLY]: 2,
[PremiumTier.BASIC_ANNUALLY]: 1.5,
[PremiumTier.PRO_MONTHLY]: 3,
[PremiumTier.PRO_ANNUALLY]: 2.5,
[PremiumTier.BUSINESS_MONTHLY]: 3.5,
Expand All @@ -28,16 +32,28 @@ 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",
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",
Expand All @@ -59,11 +75,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",
],
Expand Down Expand Up @@ -96,9 +110,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",
],
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(app)/usage/usage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: <CoinsIcon className="h-4 w-4" />,
Expand Down
1 change: 1 addition & 0 deletions apps/web/app/api/user/me/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ async function getUser(userId: string) {
lemonSqueezySubscriptionId: true,
lemonSqueezyRenewsAt: true,
unsubscribeCredits: true,
bulkUnsubscribeAccess: true,
aiAutomationAccess: true,
coldEmailBlockerAccess: true,
tier: true,
Expand Down
33 changes: 17 additions & 16 deletions apps/web/components/PremiumAlert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,31 @@ import { PremiumTier } from "@prisma/client";

export function usePremium() {
const swrResponse = useSWR<UserResponse>("/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,
};
Expand Down
11 changes: 8 additions & 3 deletions apps/web/env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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().default(""),
NEXT_PUBLIC_BASIC_ANNUALLY_PAYMENT_LINK: z.string().default(""),
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(""),
Expand All @@ -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"),
Expand Down Expand Up @@ -121,8 +126,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,
Expand Down
5 changes: 4 additions & 1 deletion apps/web/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -342,6 +343,8 @@ enum ColdEmailSetting {
}

enum PremiumTier {
BASIC_MONTHLY
BASIC_ANNUALLY
PRO_MONTHLY
PRO_ANNUALLY
BUSINESS_MONTHLY
Expand Down
2 changes: 1 addition & 1 deletion apps/web/utils/actions/premium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
});
Expand Down
24 changes: 17 additions & 7 deletions apps/web/utils/premium/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = !!(
Expand All @@ -60,7 +68,7 @@ export const hasAiAccess = (
};

export const hasColdEmailAccess = (
coldEmailBlockerAccess?: Premium["coldEmailBlockerAccess"],
coldEmailBlockerAccess?: FeatureAccess | null,
openAIApiKey?: string | null,
) => {
const hasColdEmailAccess = !!(
Expand All @@ -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;
Expand Down
27 changes: 20 additions & 7 deletions apps/web/utils/premium/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -81,6 +78,7 @@ export async function cancelPremium(options: {
where: { id: options.premiumId },
data: {
lemonSqueezyRenewsAt: options.lemonSqueezyEndsAt,
bulkUnsubscribeAccess: null,
aiAutomationAccess: null,
coldEmailBlockerAccess: null,
},
Expand Down Expand Up @@ -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}`);
}
Expand Down
2 changes: 1 addition & 1 deletion turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit a2f0254

Please sign in to comment.