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
2 changes: 1 addition & 1 deletion apps/web/app/(app)/[emailAccountId]/automation/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export default async function AutomationPage({
<PermissionsCheck />

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

<div className="flex items-center justify-between">
<div>
Expand Down
64 changes: 34 additions & 30 deletions apps/web/app/(app)/[emailAccountId]/clean/IntroStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { TypographyH3 } from "@/components/Typography";
import { Button } from "@/components/ui/button";
import { useStep } from "@/app/(app)/[emailAccountId]/clean/useStep";
import { CleanAction } from "@prisma/client";
import { PremiumAlertWithData } from "@/components/PremiumAlert";

export function IntroStep({
unhandledCount,
Expand All @@ -17,41 +18,44 @@ export function IntroStep({
const { onNext } = useStep();

return (
<div className="text-center">
<Image
src="/images/illustrations/home-office.svg"
alt="clean up"
width={200}
height={200}
className="mx-auto dark:brightness-90 dark:invert"
unoptimized
/>
<div>
<PremiumAlertWithData className="mb-20" activeOnly />
<div className="text-center">
<Image
src="/images/illustrations/home-office.svg"
alt="clean up"
width={200}
height={200}
className="mx-auto dark:brightness-90 dark:invert"
unoptimized
/>

<TypographyH3 className="mt-2">
Let's get your inbox cleaned up in 5 minutes
</TypographyH3>
<TypographyH3 className="mt-2">
Let's get your inbox cleaned up in 5 minutes
</TypographyH3>

{unhandledCount === null ? (
<SectionDescription className="mx-auto mt-2 max-w-prose">
Checking your inbox...
</SectionDescription>
) : (
<>
{unhandledCount === null ? (
<SectionDescription className="mx-auto mt-2 max-w-prose">
You have {unhandledCount.toLocaleString()}{" "}
{cleanAction === CleanAction.ARCHIVE ? "unarchived" : "unread"}{" "}
emails in your inbox.
Checking your inbox...
</SectionDescription>
<SectionDescription className="mx-auto mt-2 max-w-prose">
Let's clean up your inbox while keeping important emails safe.
</SectionDescription>
</>
)}
) : (
<>
<SectionDescription className="mx-auto mt-2 max-w-prose">
You have {unhandledCount.toLocaleString()}{" "}
{cleanAction === CleanAction.ARCHIVE ? "unarchived" : "unread"}{" "}
emails in your inbox.
</SectionDescription>
<SectionDescription className="mx-auto mt-2 max-w-prose">
Let's clean up your inbox while keeping important emails safe.
</SectionDescription>
</>
)}

<div className="mt-6">
<Button onClick={onNext} disabled={unhandledCount === null}>
Next
</Button>
<div className="mt-6">
<Button onClick={onNext} disabled={unhandledCount === null}>
Next
</Button>
Comment on lines +37 to +57
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

🧩 Analysis chain

Type mismatch: unhandledCount compared to null but typed as number.

Either change the prop type to number | null or remove the null checks. Prefer widening the type.

Proposed signature (outside this hunk):

export function IntroStep({
  unhandledCount,
  cleanAction,
}: {
  unhandledCount: number | null;
  cleanAction: CleanAction;
}) { /* ... */ }

🏁 Script executed:

#!/bin/bash
# Verify the IntroStep prop type and all call sites expect number|null
rg -nC2 'IntroStep\(' apps/web | sed -n '1,200p'
rg -nP 'unhandledCount:\s*(number( \| null)?)' --type=ts --type=tsx

Length of output: 920


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "---- locate export function IntroStep occurrences ----"
rg -n --hidden -S 'export function IntroStep' apps/web || true

echo
target='apps/web/app/(app)/[emailAccountId]/clean/IntroStep.tsx'
echo "---- show: $target ----"
if [ -f "$target" ]; then
  sed -n '1,300p' "$target"
else
  echo "File not found: $target"
fi

echo
echo "---- search for other files mentioning IntroStep (usages/definitions) ----"
rg -n --hidden -S '\bIntroStep\b' apps/web || true

echo
echo "---- search for 'unhandledCount' usages across repo ----"
rg -n --hidden -S '\bunhandledCount\b' apps/web || true

echo
echo "---- search for Props/type declarations referencing unhandledCount or IntroStep props ----"
rg -n --hidden -S 'unhandledCount|IntroStepProps|Intro.*Props|interface .*Props|type .*Props' apps/web || true

Length of output: 17404


🏁 Script executed:

#!/bin/bash
set -euo pipefail
file='apps/web/app/(app)/[emailAccountId]/clean/onboarding/page.tsx'
echo "---- show: $file ----"
if [ -f "$file" ]; then
  sed -n '1,240p' "$file"
else
  echo "File not found: $file"
fi

Length of output: 3258


🏁 Script executed:

#!/bin/bash
set -euo pipefail
file='apps/web/utils/assess.ts'
echo "---- show: $file (lines 1-240) ----"
if [ -f "$file" ]; then
  sed -n '1,240p' "$file"
else
  echo "File not found: $file"
fi

Length of output: 5372


Make IntroStep.unhandledCount nullable (number | null)

IntroStep compares unhandledCount to null (e.g. unhandledCount === null and disabled={unhandledCount === null}) but the prop is declared as number — this produces a TypeScript "condition always false" error. Widen the prop to number | null.

File: apps/web/app/(app)/[emailAccountId]/clean/IntroStep.tsx — change the prop signature:

export function IntroStep({
  unhandledCount,
  cleanAction,
}: {
  unhandledCount: number | null;
  cleanAction: CleanAction;
}) { /* ... */ }
🤖 Prompt for AI Agents
In apps/web/app/(app)/[emailAccountId]/clean/IntroStep.tsx around lines 37 to
57, the component treats unhandledCount as possibly null but its prop type is
declared as number, causing a TS "condition always false" error; change the prop
signature to widen unhandledCount to number | null (update the function props
type to accept unhandledCount: number | null) so the null checks and disabled
prop are valid, and run TypeScript to ensure no other call sites need updates.

</div>
</div>
</div>
);
Expand Down
10 changes: 9 additions & 1 deletion apps/web/app/api/clean/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import { SafeError } from "@/utils/error";
import { createScopedLogger } from "@/utils/logger";
import { aiClean } from "@/utils/ai/clean/ai-clean";
import { getEmailForLLM } from "@/utils/get-email-from-message";
import { getEmailAccountWithAiAndTokens } from "@/utils/user/get";
import {
getEmailAccountWithAiAndTokens,
getUserPremium,
} from "@/utils/user/get";
import { findUnsubscribeLink } from "@/utils/parse/parseHtml.server";
import { getCalendarEventStatus } from "@/utils/parse/calender-event";
import { GmailLabel } from "@/utils/gmail/label";
Expand All @@ -20,6 +23,7 @@ import { saveThread, updateThread } from "@/utils/redis/clean";
import { internalDateToDate } from "@/utils/date";
import { CleanAction } from "@prisma/client";
import type { ParsedMessage } from "@/utils/types";
import { isActivePremium } from "@/utils/premium";

const logger = createScopedLogger("api/clean");

Expand Down Expand Up @@ -67,6 +71,10 @@ async function cleanThread({
if (!emailAccount.tokens.access_token || !emailAccount.tokens.refresh_token)
throw new SafeError("No Gmail account found", 404);

const premium = await getUserPremium({ userId: emailAccount.userId });
if (!premium) throw new SafeError("User not premium");
if (!isActivePremium(premium)) throw new SafeError("Premium not active");

const gmail = await getGmailClientWithRefresh({
accessToken: emailAccount.tokens.access_token,
refreshToken: emailAccount.tokens.refresh_token,
Expand Down
32 changes: 31 additions & 1 deletion apps/web/components/PremiumAlert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,35 @@ export function PremiumAiAssistantAlert({
showSetApiKey,
className,
tier,
stripeSubscriptionStatus,
activeOnly,
}: {
showSetApiKey: boolean;
className?: string;
tier?: PremiumTier | null;
stripeSubscriptionStatus?: string | null;
activeOnly?: boolean;
}) {
const { PremiumModal, openModal } = usePremiumModal();

const isBasicPlan =
tier === PremiumTier.BASIC_MONTHLY || tier === PremiumTier.BASIC_ANNUALLY;

const isStripeTrialing =
stripeSubscriptionStatus && stripeSubscriptionStatus !== "active";

if (activeOnly && isStripeTrialing) {
return (
<div className={className}>
<ActionCard
icon={<CrownIcon className="h-5 w-5" />}
title="Active Subscription Required"
description="This feature is not available on trial plans."
/>
</div>
);
}

return (
<div className={className}>
{isBasicPlan ? (
Expand Down Expand Up @@ -88,12 +107,19 @@ export function PremiumAiAssistantAlert({
);
}

export function PremiumAlertWithData({ className }: { className?: string }) {
export function PremiumAlertWithData({
className,
activeOnly,
}: {
className?: string;
activeOnly?: boolean;
}) {
const {
hasAiAccess,
isLoading: isLoadingPremium,
isProPlanWithoutApiKey,
tier,
data,
} = usePremium();

if (!isLoadingPremium && !hasAiAccess) {
Expand All @@ -102,6 +128,10 @@ export function PremiumAlertWithData({ className }: { className?: string }) {
showSetApiKey={isProPlanWithoutApiKey}
className={className}
tier={tier}
stripeSubscriptionStatus={
data?.premium?.stripeSubscriptionStatus || null
}
activeOnly={activeOnly}
/>
);
}
Expand Down
8 changes: 7 additions & 1 deletion apps/web/utils/actions/clean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import { actionClient } from "@/utils/actions/safe-action";
import { SafeError } from "@/utils/error";
import { createEmailProvider } from "@/utils/email/provider";
import { isGoogleProvider } from "@/utils/email/provider-types";
import { getUserPremium } from "@/utils/user/get";
import { isActivePremium } from "@/utils/premium";

const logger = createScopedLogger("actions/clean");

Expand All @@ -36,7 +38,7 @@ export const cleanInboxAction = actionClient
.schema(cleanInboxSchema)
.action(
async ({
ctx: { emailAccountId, provider },
ctx: { emailAccountId, provider, userId },
parsedInput: { action, instructions, daysOld, skips, maxEmails },
}) => {
if (!isGoogleProvider(provider)) {
Expand All @@ -45,6 +47,10 @@ export const cleanInboxAction = actionClient
);
}

const premium = await getUserPremium({ userId });
if (!premium) throw new SafeError("User not premium");
if (!isActivePremium(premium)) throw new SafeError("Premium not active");

const gmail = await getGmailClientForEmail({ emailAccountId });
const emailProvider = await createEmailProvider({
emailAccountId,
Expand Down
14 changes: 14 additions & 0 deletions apps/web/utils/premium/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ export const isPremium = (
);
};

export const isActivePremium = (
premium: Pick<
Premium,
"lemonSqueezyRenewsAt" | "stripeSubscriptionStatus"
> | null,
): boolean => {
if (!premium) return false;

return (
premium.stripeSubscriptionStatus === "active" ||
isPremiumLemonSqueezy(premium.lemonSqueezyRenewsAt)
);
};

export const getUserTier = (
premium?: Pick<
Premium,
Expand Down
9 changes: 9 additions & 0 deletions apps/web/utils/user/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,15 @@ export async function getEmailAccountWithAiAndTokens({
};
}

export async function getUserPremium({ userId }: { userId: string }) {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { premium: true },
});

return user?.premium || null;
}

export async function getWritingStyle({
emailAccountId,
}: {
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v2.9.40
v2.9.41
Loading