diff --git a/apps/web/.env.example b/apps/web/.env.example index b8361be673..787d0e0397 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -57,6 +57,9 @@ NEXT_PUBLIC_BUSINESS_ANNUALLY_PAYMENT_LINK=# NEXT_PUBLIC_BUSINESS_MONTHLY_VARIANT_ID=123 NEXT_PUBLIC_BUSINESS_ANNUALLY_VARIANT_ID=123 +NEXT_PUBLIC_COPILOT_MONTHLY_PAYMENT_LINK=# +NEXT_PUBLIC_COPILOT_MONTHLY_VARIANT_ID=123 + NEXT_PUBLIC_LIFETIME_PAYMENT_LINK=# NEXT_PUBLIC_LIFETIME_VARIANT_ID=123 NEXT_PUBLIC_LIFETIME_EXTRA_SEATS_PAYMENT_LINK=# diff --git a/apps/web/app/(app)/admin/AdminUpgradeUserForm.tsx b/apps/web/app/(app)/admin/AdminUpgradeUserForm.tsx index de6e038959..1164d88217 100644 --- a/apps/web/app/(app)/admin/AdminUpgradeUserForm.tsx +++ b/apps/web/app/(app)/admin/AdminUpgradeUserForm.tsx @@ -89,6 +89,10 @@ export const AdminUpgradeUserForm = () => { label: PremiumTier.BASIC_MONTHLY, value: PremiumTier.BASIC_MONTHLY, }, + { + label: PremiumTier.COPILOT_MONTHLY, + value: PremiumTier.COPILOT_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 f6e64df7f3..e8065a81eb 100644 --- a/apps/web/app/(app)/admin/validation.tsx +++ b/apps/web/app/(app)/admin/validation.tsx @@ -18,6 +18,7 @@ export const changePremiumStatusSchema = z.object({ PremiumTier.PRO_ANNUALLY, PremiumTier.BUSINESS_MONTHLY, PremiumTier.BUSINESS_ANNUALLY, + PremiumTier.COPILOT_MONTHLY, PremiumTier.LIFETIME, ]), upgrade: z.boolean(), diff --git a/apps/web/app/(app)/license/page.tsx b/apps/web/app/(app)/license/page.tsx index 17f604902a..39e87aefce 100644 --- a/apps/web/app/(app)/license/page.tsx +++ b/apps/web/app/(app)/license/page.tsx @@ -5,7 +5,6 @@ import { type SubmitHandler, useForm } from "react-hook-form"; import useSWR from "swr"; import { Button } from "@/components/Button"; import { Input } from "@/components/Input"; -import { toastSuccess, toastError } from "@/components/Toast"; import { TopSection } from "@/components/TopSection"; import { activateLicenseKeyAction } from "@/utils/actions/premium"; import type { UserResponse } from "@/app/api/user/me/route"; diff --git a/apps/web/app/(app)/premium/Pricing.tsx b/apps/web/app/(app)/premium/Pricing.tsx index 0ce4e53fd5..0a3a8dce49 100644 --- a/apps/web/app/(app)/premium/Pricing.tsx +++ b/apps/web/app/(app)/premium/Pricing.tsx @@ -133,11 +133,15 @@ export function Pricing() {
- Save up to 40%! + {/* Save up to 40%! */} + Save 32%!
-
+ {/* 3 col layout */} + {/*
*/} + {/* 2 col layout */} +
{tiers.map((tier, tierIdx) => { const isCurrentPlan = tier.tiers?.[frequency.value] === premiumTier; @@ -160,9 +164,11 @@ export function Pricing() {
@@ -186,11 +192,9 @@ export function Pricing() { ${tier.price[frequency.value]} - {!tier.hideFrequency && ( - - {frequency.priceSuffix} - - )} + + {frequency.priceSuffix} + {!!tier.discount?.[frequency.value] && ( diff --git a/apps/web/app/(app)/premium/config.ts b/apps/web/app/(app)/premium/config.ts index c5d66dc43e..8bc58fc430 100644 --- a/apps/web/app/(app)/premium/config.ts +++ b/apps/web/app/(app)/premium/config.ts @@ -13,6 +13,7 @@ export const pricing: Record = { [PremiumTier.PRO_ANNUALLY]: 9, [PremiumTier.BUSINESS_MONTHLY]: 22, [PremiumTier.BUSINESS_ANNUALLY]: 15, + [PremiumTier.COPILOT_MONTHLY]: 99, [PremiumTier.LIFETIME]: 299, }; @@ -23,6 +24,7 @@ export const pricingAdditonalEmail: Record = { [PremiumTier.PRO_ANNUALLY]: 2.5, [PremiumTier.BUSINESS_MONTHLY]: 3.5, [PremiumTier.BUSINESS_ANNUALLY]: 3, + [PremiumTier.COPILOT_MONTHLY]: 0, [PremiumTier.LIFETIME]: 59, }; @@ -30,106 +32,151 @@ function discount(monthly: number, annually: number) { return ((monthly - annually) / monthly) * 100; } -export const tiers = [ - { - 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", - "Unlimited unsubscribes", - "Unlimited archives", - "Email analytics", - ], - cta: "Upgrade", - }, - { - name: "Pro", - tiers: { - monthly: PremiumTier.PRO_MONTHLY, - annually: PremiumTier.PRO_ANNUALLY, - }, - href: { - monthly: env.NEXT_PUBLIC_PRO_MONTHLY_PAYMENT_LINK, - annually: env.NEXT_PUBLIC_PRO_ANNUALLY_PAYMENT_LINK, - }, - price: { monthly: pricing.PRO_MONTHLY, annually: pricing.PRO_ANNUALLY }, - priceAdditional: { - monthly: pricingAdditonalEmail.PRO_MONTHLY, - annually: pricingAdditonalEmail.PRO_ANNUALLY, - }, - discount: { - monthly: 0, - annually: discount(pricing.PRO_MONTHLY, pricing.PRO_ANNUALLY), - }, - description: "Unlock AI features when using your own OpenAI key", - features: [ - "Everything in free", - "AI automation when using your own OpenAI API key", - "Cold email blocker when using your own OpenAI API key", - ], - cta: "Upgrade", - mostPopular: false, - }, - { - name: "Business", - tiers: { - monthly: PremiumTier.BUSINESS_MONTHLY, - annually: PremiumTier.BUSINESS_ANNUALLY, - }, - href: { - monthly: env.NEXT_PUBLIC_BUSINESS_MONTHLY_PAYMENT_LINK, - annually: env.NEXT_PUBLIC_BUSINESS_ANNUALLY_PAYMENT_LINK, - }, - price: { - monthly: pricing.BUSINESS_MONTHLY, - annually: pricing.BUSINESS_ANNUALLY, - }, - priceAdditional: { - monthly: pricingAdditonalEmail.BUSINESS_MONTHLY, - annually: pricingAdditonalEmail.BUSINESS_ANNUALLY, - }, - discount: { - monthly: 0, - annually: discount(pricing.BUSINESS_MONTHLY, pricing.BUSINESS_ANNUALLY), - }, - description: "Unlock full AI-powered email management", - features: [ - "Everything in pro", - "Unlimited AI credits", - "No need to provide your own OpenAI API key", - "Priority support", - ], - cta: "Upgrade", - mostPopular: true, - hideFrequency: false, - }, - // { - // name: "Enterprise", - // id: "tier-enterprise", - // href: env.NEXT_PUBLIC_CALL_LINK, - // price: { monthly: "Book a call", annually: "Book a call" }, - // description: "For help self-hosting, and dedicated support.", - // features: ["Self-hosted", "Everything in pro", "Dedicated support"], - // hideFrequency: true, - // cta: "Book a call", - // }, +const basicTier = { + 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", + "Unlimited unsubscribes", + "Unlimited archives", + "Email analytics", + ], + cta: "Upgrade", +}; + +const proTier = { + name: "Pro", + tiers: { + monthly: PremiumTier.PRO_MONTHLY, + annually: PremiumTier.PRO_ANNUALLY, + }, + href: { + monthly: env.NEXT_PUBLIC_PRO_MONTHLY_PAYMENT_LINK, + annually: env.NEXT_PUBLIC_PRO_ANNUALLY_PAYMENT_LINK, + }, + price: { monthly: pricing.PRO_MONTHLY, annually: pricing.PRO_ANNUALLY }, + priceAdditional: { + monthly: pricingAdditonalEmail.PRO_MONTHLY, + annually: pricingAdditonalEmail.PRO_ANNUALLY, + }, + discount: { + monthly: 0, + annually: discount(pricing.PRO_MONTHLY, pricing.PRO_ANNUALLY), + }, + description: "Unlock AI features when using your own OpenAI key", + features: [ + "Everything in free", + "AI automation when using your own OpenAI API key", + "Cold email blocker when using your own OpenAI API key", + ], + cta: "Upgrade", + mostPopular: false, +}; + +const businessTier = { + name: "Business", + tiers: { + monthly: PremiumTier.BUSINESS_MONTHLY, + annually: PremiumTier.BUSINESS_ANNUALLY, + }, + href: { + monthly: env.NEXT_PUBLIC_BUSINESS_MONTHLY_PAYMENT_LINK, + annually: env.NEXT_PUBLIC_BUSINESS_ANNUALLY_PAYMENT_LINK, + }, + price: { + monthly: pricing.BUSINESS_MONTHLY, + annually: pricing.BUSINESS_ANNUALLY, + }, + priceAdditional: { + monthly: pricingAdditonalEmail.BUSINESS_MONTHLY, + annually: pricingAdditonalEmail.BUSINESS_ANNUALLY, + }, + discount: { + monthly: 0, + annually: discount(pricing.BUSINESS_MONTHLY, pricing.BUSINESS_ANNUALLY), + }, + description: "Unlock full AI-powered email management", + // features: [ + // "Everything in pro", + // "Unlimited AI credits", + // "No need to provide your own OpenAI API key", + // "Priority support", + // ], + features: [ + "AI automation", + "Bulk email unsubscriber", + "Cold email blocker", + "Email analytics", + "Unlimited AI credits", + "Priority support", + ], + cta: "Upgrade", + mostPopular: true, +}; + +const copilotTier = { + name: "Co-Pilot", + tiers: { + monthly: PremiumTier.COPILOT_MONTHLY, + annually: PremiumTier.COPILOT_MONTHLY, + }, + href: { + monthly: env.NEXT_PUBLIC_COPILOT_MONTHLY_PAYMENT_LINK, + annually: env.NEXT_PUBLIC_COPILOT_MONTHLY_PAYMENT_LINK, + }, + price: { + monthly: pricing.COPILOT_MONTHLY, + annually: pricing.COPILOT_MONTHLY, + }, + priceAdditional: { + monthly: pricingAdditonalEmail.COPILOT_MONTHLY, + annually: pricingAdditonalEmail.COPILOT_MONTHLY, + }, + discount: { monthly: 0, annually: 0 }, + description: + "Get a 30-minute monthly call to help you get your email organized", + features: [ + "Everything in Business", + "30-minute 1:1 monthly call to help you get your email organized", + "Full refund if not satisfied", + ], + cta: "Upgrade", + mostPopular: false, +}; + +export const tiers: { + name: string; + tiers: { monthly: PremiumTier; annually: PremiumTier }; + href: { monthly: string; annually: string }; + price: { monthly: number; annually: number }; + priceAdditional: { monthly: number; annually: number }; + discount: { monthly: number; annually: number }; + description: string; + features: string[]; + cta: string; + mostPopular?: boolean; +}[] = [ + // basicTier, + // proTier, + businessTier, + copilotTier, ]; export const lifetimeFeatures = [ diff --git a/apps/web/app/(app)/settings/LabelsSection.tsx b/apps/web/app/(app)/settings/LabelsSection.tsx index 8a09670f6f..365cc219e0 100644 --- a/apps/web/app/(app)/settings/LabelsSection.tsx +++ b/apps/web/app/(app)/settings/LabelsSection.tsx @@ -209,7 +209,7 @@ function LabelsSectionFormInner(props: {
Suggested Labels - Labels we suggest adding to organise your emails. Click a label + Labels we suggest adding to organize your emails. Click a label to add it.
diff --git a/apps/web/app/api/lemon-squeezy/webhook/route.ts b/apps/web/app/api/lemon-squeezy/webhook/route.ts index a9143608f3..a82887be76 100644 --- a/apps/web/app/api/lemon-squeezy/webhook/route.ts +++ b/apps/web/app/api/lemon-squeezy/webhook/route.ts @@ -294,6 +294,9 @@ function getSubscriptionTier({ return PremiumTier.BUSINESS_MONTHLY; case env.NEXT_PUBLIC_BUSINESS_ANNUALLY_VARIANT_ID: return PremiumTier.BUSINESS_ANNUALLY; + + case env.NEXT_PUBLIC_COPILOT_MONTHLY_VARIANT_ID: + return PremiumTier.COPILOT_MONTHLY; } throw new Error(`Unknown variant id: ${variantId}`); diff --git a/apps/web/app/blog/post/how-my-open-source-saas-hit-first-on-product-hunt/content.mdx b/apps/web/app/blog/post/how-my-open-source-saas-hit-first-on-product-hunt/content.mdx index daf3fa9189..81e48afefa 100644 --- a/apps/web/app/blog/post/how-my-open-source-saas-hit-first-on-product-hunt/content.mdx +++ b/apps/web/app/blog/post/how-my-open-source-saas-hit-first-on-product-hunt/content.mdx @@ -172,7 +172,7 @@ Organize a list of people that will support your launch. My list was 100 people. You can also add communities to the list. Many Slack or Discord communities have a channel to promote yourself. These are a good places to get some extra eyeballs. And of course LinkedIn, Twitter, Facebook groups, and other social media platforms you’re on. -You could skip organizing the list ahead of time. But it adds a bit less pressure to the day of the launch if you have it organised. +You could skip organizing the list ahead of time. But it adds a bit less pressure to the day of the launch if you have it organized. I used a mini Notion CRM myself and had a status column for each person. diff --git a/apps/web/env.ts b/apps/web/env.ts index c7d19e180c..03fe64b329 100644 --- a/apps/web/env.ts +++ b/apps/web/env.ts @@ -66,6 +66,9 @@ export const env = createEnv({ NEXT_PUBLIC_BUSINESS_ANNUALLY_PAYMENT_LINK: z.string().default(""), NEXT_PUBLIC_BUSINESS_MONTHLY_VARIANT_ID: z.coerce.number().default(0), NEXT_PUBLIC_BUSINESS_ANNUALLY_VARIANT_ID: z.coerce.number().default(0), + // copilot + NEXT_PUBLIC_COPILOT_MONTHLY_PAYMENT_LINK: z.string().default(""), + NEXT_PUBLIC_COPILOT_MONTHLY_VARIANT_ID: z.coerce.number().default(0), // lifetime NEXT_PUBLIC_LIFETIME_PAYMENT_LINK: z.string().default(""), NEXT_PUBLIC_LIFETIME_VARIANT_ID: z.coerce.number().default(0), @@ -121,6 +124,11 @@ export const env = createEnv({ process.env.NEXT_PUBLIC_BUSINESS_MONTHLY_VARIANT_ID, NEXT_PUBLIC_BUSINESS_ANNUALLY_VARIANT_ID: process.env.NEXT_PUBLIC_BUSINESS_ANNUALLY_VARIANT_ID, + // copilot + NEXT_PUBLIC_COPILOT_MONTHLY_PAYMENT_LINK: + process.env.NEXT_PUBLIC_COPILOT_MONTHLY_PAYMENT_LINK, + NEXT_PUBLIC_COPILOT_MONTHLY_VARIANT_ID: + process.env.NEXT_PUBLIC_COPILOT_MONTHLY_VARIANT_ID, // lifetime NEXT_PUBLIC_LIFETIME_PAYMENT_LINK: process.env.NEXT_PUBLIC_LIFETIME_PAYMENT_LINK, diff --git a/apps/web/prisma/migrations/20240730122310_copilot_tier/migration.sql b/apps/web/prisma/migrations/20240730122310_copilot_tier/migration.sql new file mode 100644 index 0000000000..2d73bced1b --- /dev/null +++ b/apps/web/prisma/migrations/20240730122310_copilot_tier/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "PremiumTier" ADD VALUE 'COPILOT_MONTHLY'; diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma index 199856d632..b1373a022d 100644 --- a/apps/web/prisma/schema.prisma +++ b/apps/web/prisma/schema.prisma @@ -374,6 +374,7 @@ enum PremiumTier { PRO_ANNUALLY BUSINESS_MONTHLY BUSINESS_ANNUALLY + COPILOT_MONTHLY LIFETIME } diff --git a/apps/web/utils/actions/premium.ts b/apps/web/utils/actions/premium.ts index b4739bedbf..a4aa50b580 100644 --- a/apps/web/utils/actions/premium.ts +++ b/apps/web/utils/actions/premium.ts @@ -274,7 +274,8 @@ export async function changePremiumStatusAction( options.period === PremiumTier.BUSINESS_ANNUALLY ? new Date(+new Date() + ONE_MONTH * 12) : options.period === PremiumTier.PRO_MONTHLY || - options.period === PremiumTier.BUSINESS_MONTHLY + options.period === PremiumTier.BUSINESS_MONTHLY || + options.period === PremiumTier.COPILOT_MONTHLY ? new Date(+new Date() + ONE_MONTH) : null, emailAccountsAccess: options.emailAccountsAccess, diff --git a/apps/web/utils/premium/index.ts b/apps/web/utils/premium/index.ts index cda2504082..9ae9911fbb 100644 --- a/apps/web/utils/premium/index.ts +++ b/apps/web/utils/premium/index.ts @@ -91,7 +91,8 @@ export function isOnHigherTier( [PremiumTier.PRO_ANNUALLY]: 4, [PremiumTier.BUSINESS_MONTHLY]: 5, [PremiumTier.BUSINESS_ANNUALLY]: 6, - [PremiumTier.LIFETIME]: 7, + [PremiumTier.COPILOT_MONTHLY]: 7, + [PremiumTier.LIFETIME]: 8, }; const tier1Rank = tier1 ? tierRanking[tier1] : 0; diff --git a/apps/web/utils/premium/server.ts b/apps/web/utils/premium/server.ts index c6e321bcba..80191fdeda 100644 --- a/apps/web/utils/premium/server.ts +++ b/apps/web/utils/premium/server.ts @@ -129,6 +129,7 @@ function getTierAccess(tier: PremiumTier) { }; case PremiumTier.BUSINESS_MONTHLY: case PremiumTier.BUSINESS_ANNUALLY: + case PremiumTier.COPILOT_MONTHLY: case PremiumTier.LIFETIME: return { bulkUnsubscribeAccess: FeatureAccess.UNLOCKED, diff --git a/turbo.json b/turbo.json index 3dd13da919..1dfa869f6f 100644 --- a/turbo.json +++ b/turbo.json @@ -38,27 +38,36 @@ "ADMINS", "NEXT_PUBLIC_LEMON_STORE_ID", + "NEXT_PUBLIC_BASIC_MONTHLY_PAYMENT_LINK", "NEXT_PUBLIC_BASIC_ANNUALLY_PAYMENT_LINK", "NEXT_PUBLIC_BASIC_MONTHLY_VARIANT_ID", "NEXT_PUBLIC_BASIC_ANNUALLY_VARIANT_ID", + "NEXT_PUBLIC_PRO_MONTHLY_PAYMENT_LINK", "NEXT_PUBLIC_PRO_ANNUALLY_PAYMENT_LINK", "NEXT_PUBLIC_PRO_MONTHLY_VARIANT_ID", "NEXT_PUBLIC_PRO_ANNUALLY_VARIANT_ID", + "NEXT_PUBLIC_BUSINESS_MONTHLY_PAYMENT_LINK", "NEXT_PUBLIC_BUSINESS_ANNUALLY_PAYMENT_LINK", "NEXT_PUBLIC_BUSINESS_MONTHLY_VARIANT_ID", "NEXT_PUBLIC_BUSINESS_ANNUALLY_VARIANT_ID", + + "NEXT_PUBLIC_COPILOT_MONTHLY_PAYMENT_LINK", + "NEXT_PUBLIC_COPILOT_MONTHLY_VARIANT_ID", + "NEXT_PUBLIC_LIFETIME_PAYMENT_LINK", "NEXT_PUBLIC_LIFETIME_VARIANT_ID", "NEXT_PUBLIC_LIFETIME_EXTRA_SEATS_PAYMENT_LINK", "NEXT_PUBLIC_LIFETIME_EXTRA_SEATS_VARIANT_ID", + "LICENSE_1_SEAT_VARIANT_ID", "LICENSE_3_SEAT_VARIANT_ID", "LICENSE_5_SEAT_VARIANT_ID", "LICENSE_10_SEAT_VARIANT_ID", "LICENSE_25_SEAT_VARIANT_ID", + "NEXT_PUBLIC_FREE_UNSUBSCRIBE_CREDITS", "NEXT_PUBLIC_CALL_LINK", "NEXT_PUBLIC_POSTHOG_KEY",