Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions apps/web/app/(app)/[emailAccountId]/automation/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { PermissionsCheck } from "@/app/(app)/[emailAccountId]/PermissionsCheck"
import { EmailProvider } from "@/providers/EmailProvider";
import { ASSISTANT_ONBOARDING_COOKIE } from "@/utils/cookies";
import { prefixPath } from "@/utils/path";
import { PremiumAlertWithData } from "@/components/PremiumAlert";
import { checkUserOwnsEmailAccount } from "@/utils/email-account";
import { SettingsTab } from "@/app/(app)/[emailAccountId]/assistant/settings/SettingsTab";
import { TabSelect } from "@/components/TabSelect";
Expand Down Expand Up @@ -86,8 +85,6 @@ export default async function AutomationPage({
<PermissionsCheck />

<PageWrapper>
<PremiumAlertWithData className="mb-8" />

<div className="flex items-center justify-between">
<div>
<PageHeader
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Suspense } from "react";
import { PremiumAlertWithData } from "@/components/PremiumAlert";
import { PermissionsCheck } from "@/app/(app)/[emailAccountId]/PermissionsCheck";
import { GmailProvider } from "@/providers/GmailProvider";
import { ColdEmailContent } from "@/app/(app)/[emailAccountId]/cold-email-blocker/ColdEmailContent";
Expand All @@ -9,10 +8,6 @@ export default function ColdEmailBlockerPage() {
<GmailProvider>
<Suspense>
<PermissionsCheck />
<div className="content-container">
<PremiumAlertWithData className="mt-2" />
</div>

<ColdEmailContent isInset />
</Suspense>
</GmailProvider>
Expand Down
77 changes: 77 additions & 0 deletions apps/web/app/(landing)/components/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { SettingCard } from "@/components/SettingCard";
import { IconCircle } from "@/app/(app)/[emailAccountId]/onboarding/IconCircle";
import { ActionBadges } from "@/app/(app)/[emailAccountId]/assistant/Rules";
import { DismissibleVideoCard } from "@/components/VideoCard";
import { PremiumExpiredCardContent } from "@/components/PremiumExpiredCard";

export const maxDuration = 3;

Expand Down Expand Up @@ -381,6 +382,82 @@ export default function Components() {
</div>
</div>

<div>
<div className="underline">Premium Expired Banners</div>
<div className="mt-4 space-y-4">
<div>
<p className="mb-2 text-sm text-muted-foreground">
Stripe Past Due:
</p>
<PremiumExpiredCardContent
premium={{
lemonSqueezyRenewsAt: null,
stripeSubscriptionId: "sub_test123",
stripeSubscriptionStatus: "past_due",
lemonSqueezySubscriptionId: null,
tier: "PRO_MONTHLY",
}}
/>
</div>
<div>
<p className="mb-2 text-sm text-muted-foreground">
Stripe Canceled:
</p>
<PremiumExpiredCardContent
premium={{
lemonSqueezyRenewsAt: null,
stripeSubscriptionId: "sub_test456",
stripeSubscriptionStatus: "canceled",
lemonSqueezySubscriptionId: null,
tier: "BUSINESS_MONTHLY",
}}
/>
</div>
<div>
<p className="mb-2 text-sm text-muted-foreground">
LemonSqueezy Expired:
</p>
<PremiumExpiredCardContent
premium={{
lemonSqueezyRenewsAt: new Date(
Date.now() - 24 * 60 * 60 * 1000,
), // Yesterday
stripeSubscriptionId: null,
stripeSubscriptionStatus: null,
lemonSqueezySubscriptionId: 456,
tier: "PRO_ANNUALLY",
}}
/>
</div>
<div>
<p className="mb-2 text-sm text-muted-foreground">
No Banner (Active Premium):
</p>
<div className="min-h-[20px] text-xs text-muted-foreground">
<PremiumExpiredCardContent
premium={{
lemonSqueezyRenewsAt: null,
stripeSubscriptionId: "sub_active123",
stripeSubscriptionStatus: "active",
lemonSqueezySubscriptionId: null,
tier: "BUSINESS_MONTHLY",
}}
/>
Banner should not appear for active users
</div>
</div>
<div>
<p className="mb-2 text-sm text-muted-foreground">
No Banner (Never Had Premium):
</p>
<div className="min-h-[20px] text-xs text-muted-foreground">
<PremiumExpiredCardContent premium={null} />
Banner should not appear for users who never had premium
</div>
</div>
</div>
</div>

<div className="flex gap-2">
<TestErrorButton />
<TestActionButton />
Expand Down
161 changes: 161 additions & 0 deletions apps/web/components/PremiumExpiredCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
"use client";

import { useState } from "react";
import Link from "next/link";
import { XIcon, CreditCardIcon, AlertTriangleIcon } from "lucide-react";
import { useUser } from "@/hooks/useUser";
import { isPremium } from "@/utils/premium";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { cn } from "@/utils";

interface PremiumData {
lemonSqueezyRenewsAt?: Date | string | null;
stripeSubscriptionStatus?: string | null;
stripeSubscriptionId?: string | null;
lemonSqueezySubscriptionId?: number | string | null;
tier?: string | null;
}

interface PremiumExpiredCardProps {
premium: PremiumData | null | undefined;
onDismiss?: () => void;
}

export function PremiumExpiredCardContent({
premium,
onDismiss,
}: PremiumExpiredCardProps) {
// Early return if no premium data
if (!premium) return null;

// Convert string dates to Date objects if needed
const lemonSqueezyRenewsAt = premium.lemonSqueezyRenewsAt
? typeof premium.lemonSqueezyRenewsAt === "string"
? new Date(premium.lemonSqueezyRenewsAt)
: premium.lemonSqueezyRenewsAt
: null;

const isUserPremium = isPremium(
lemonSqueezyRenewsAt,
premium.stripeSubscriptionStatus || null,
);

if (isUserPremium) return null;

// Determine the message based on subscription state
const getSubscriptionMessage = () => {
const status = premium.stripeSubscriptionStatus;
const hasLemonSqueezyExpired =
lemonSqueezyRenewsAt && lemonSqueezyRenewsAt < new Date();

if (status === "past_due") {
return {
title: "Payment Past Due",
description: "Update your payment method to continue service",
};
}

if (status === "canceled" || status === "cancelled") {
return {
title: "Subscription Cancelled",
description: "Reactivate to resume AI email management",
};
}

if (status === "incomplete" || status === "incomplete_expired") {
return {
title: "Payment Incomplete",
description: "Complete your payment to activate service",
};
}

if (status === "unpaid") {
return {
title: "Payment Required",
description: "Update payment to continue AI features",
};
}

if (hasLemonSqueezyExpired || status === "expired") {
return {
title: "Subscription Expired",
description: "Renew your subscription to continue",
};
}

// Default fallback
return {
title: "Subscription Issue",
description: "Please check your subscription status",
};
};

const { title, description } = getSubscriptionMessage();

return (
<Card
className={cn(
"border-orange-200 bg-gradient-to-tr from-transparent via-orange-50/80 to-orange-500/15 shadow-sm",
"dark:border-orange-900 dark:from-orange-950/50 dark:via-orange-900/20 dark:to-orange-800/10",
)}
>
<div className="p-3">
<div className="flex items-start gap-2">
<AlertTriangleIcon className="h-4 w-4 flex-shrink-0 text-orange-600 dark:text-orange-400 mt-0.5" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-orange-800 dark:text-orange-200 leading-tight">
{title}
</p>
<p className="text-xs text-orange-700/80 dark:text-orange-300/80 mt-1">
{description}
</p>
</div>

{onDismiss && (
<button
type="button"
className="flex-shrink-0 rounded p-1 text-orange-600 hover:bg-orange-100 focus:outline-none focus:ring-2 focus:ring-orange-300 transition-colors dark:text-orange-400 dark:hover:bg-orange-900/20 dark:focus:ring-orange-700"
onClick={onDismiss}
aria-label="Dismiss banner"
>
<XIcon className="h-3 w-3" />
</button>
)}
</div>

<div className="mt-3">
<Button
asChild
size="sm"
className="w-full bg-orange-600 text-white hover:bg-orange-700 border-0 shadow-sm h-8"
>
<Link
href="/settings"
className="flex items-center justify-center gap-1.5"
>
<CreditCardIcon className="h-3.5 w-3.5" />
<span className="text-xs font-medium">Reactivate</span>
</Link>
</Button>
</div>
</div>
</Card>
);
}

export function PremiumExpiredCard() {
const [dismissed, setDismissed] = useState(false);
const { data: user, isLoading } = useUser();

if (isLoading || dismissed || !user) return null;

return (
<div className="px-3 pt-4">
<PremiumExpiredCardContent
premium={user.premium}
onDismiss={() => setDismissed(true)}
/>
</div>
);
}
3 changes: 3 additions & 0 deletions apps/web/components/SideNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import { prefixPath } from "@/utils/path";
import { ReferralDialog } from "@/components/ReferralDialog";
import { isGoogleProvider } from "@/utils/email/provider-types";
import { NavUser } from "@/components/NavUser";
import { PremiumExpiredCard } from "@/components/PremiumExpiredCard";

type NavItem = {
name: string;
Expand Down Expand Up @@ -224,6 +225,8 @@ export function SideNav({ ...props }: React.ComponentProps<typeof Sidebar>) {
</SidebarGroupContent>
</SidebarContent>

<PremiumExpiredCard />

Comment thread
elie222 marked this conversation as resolved.
<SidebarFooter className="pb-4">
<ClientOnly>
<ReferralDialog />
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v2.10.17
v2.10.18
Loading