diff --git a/apps/web/app/(app)/accounts/AddAccount.tsx b/apps/web/app/(app)/accounts/AddAccount.tsx index ed0748cfa6..6e97dc1c84 100644 --- a/apps/web/app/(app)/accounts/AddAccount.tsx +++ b/apps/web/app/(app)/accounts/AddAccount.tsx @@ -19,13 +19,10 @@ export function AddAccount() { setLoading(true); try { - const response = await fetch( - `/api/${provider}/linking/auth-url?action=auto`, - { - method: "GET", - headers: { "Content-Type": "application/json" }, - }, - ); + const response = await fetch(`/api/${provider}/linking/auth-url`, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); if (!response.ok) { toastError({ @@ -86,8 +83,8 @@ export function AddAccount() { Add Microsoft Account - - You will be billed for each additional account + + You will be billed for each account. diff --git a/apps/web/app/(app)/accounts/page.tsx b/apps/web/app/(app)/accounts/page.tsx index b8bbc2a44c..8f08be7b5b 100644 --- a/apps/web/app/(app)/accounts/page.tsx +++ b/apps/web/app/(app)/accounts/page.tsx @@ -3,7 +3,7 @@ import { useAction } from "next-safe-action/hooks"; import Link from "next/link"; import { Trash2, ArrowRight, BotIcon } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { useSearchParams, useRouter, usePathname } from "next/navigation"; import { ConfirmDialog } from "@/components/ConfirmDialog"; import { LoadingContent } from "@/components/LoadingContent"; @@ -25,16 +25,6 @@ import { PageHeader } from "@/components/PageHeader"; import { PageWrapper } from "@/components/PageWrapper"; import { logOut } from "@/utils/user"; import { getAndClearAuthErrorCookie } from "@/utils/auth-cookies"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; export default function AccountsPage() { const { data, isLoading, error, mutate } = useAccounts(); @@ -45,7 +35,7 @@ export default function AccountsPage() { -
+
{data?.emailAccounts.map((emailAccount) => (
- - ); } @@ -145,97 +133,6 @@ function AccountItem({ ); } -function MergeConfirmationDialog() { - const searchParams = useSearchParams(); - const router = useRouter(); - const pathname = usePathname(); - const [showDialog, setShowDialog] = useState(false); - const [provider, setProvider] = useState<"google" | "microsoft">(); - const [email, setEmail] = useState(); - const [isConfirming, setIsConfirming] = useState(false); - - useEffect(() => { - const confirmMerge = searchParams.get("confirm_merge"); - const providerParam = searchParams.get("provider"); - const emailParam = searchParams.get("email"); - - if (confirmMerge === "true" && providerParam && emailParam) { - setShowDialog(true); - setProvider(providerParam as "google" | "microsoft"); - setEmail(emailParam); - router.replace(pathname); - } - }, [searchParams, router, pathname]); - - const handleConfirm = async () => { - if (!provider) return; - - setIsConfirming(true); - try { - const apiProvider = provider === "google" ? "google" : "outlook"; - const response = await fetch( - `/api/${apiProvider}/linking/auth-url?action=merge_confirmed`, - { - method: "GET", - headers: { "Content-Type": "application/json" }, - }, - ); - - if (!response.ok) { - toastError({ - title: "Error initiating merge", - description: "Please try again or contact support", - }); - setIsConfirming(false); - return; - } - - const data = await response.json(); - window.location.href = data.url; - } catch (error) { - console.error("Error initiating merge:", error); - toastError({ - title: "Error initiating merge", - description: "Please try again or contact support", - }); - setIsConfirming(false); - } - }; - - const handleCancel = () => { - setShowDialog(false); - setProvider(undefined); - setEmail(undefined); - }; - - return ( - - - - Merge Accounts? - - The {provider === "google" ? "Google" : "Microsoft"} account{" "} - {email} already has an Inbox Zero account. -
-
- Merging will combine both accounts into one. This will delete the - old account and move all its data to your current account. This - action cannot be undone. -
-
- - - Cancel - - - {isConfirming ? "Merging..." : "Merge Accounts"} - - -
-
- ); -} - function useAccountNotifications() { const searchParams = useSearchParams(); const router = useRouter(); diff --git a/apps/web/app/(landing)/components/page.tsx b/apps/web/app/(landing)/components/page.tsx index a806fb78e9..f72f35ba1c 100644 --- a/apps/web/app/(landing)/components/page.tsx +++ b/apps/web/app/(landing)/components/page.tsx @@ -38,7 +38,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"; +import { PremiumExpiredCardContent } from "@/components/PremiumCard"; import { ResultsDisplay, ResultDisplayContent, diff --git a/apps/web/app/api/google/linking/auth-url/route.ts b/apps/web/app/api/google/linking/auth-url/route.ts index 612faa2393..c39705123c 100644 --- a/apps/web/app/api/google/linking/auth-url/route.ts +++ b/apps/web/app/api/google/linking/auth-url/route.ts @@ -1,5 +1,4 @@ import { NextResponse } from "next/server"; -import { z } from "zod"; import { withAuth } from "@/utils/middleware"; import { getLinkingOAuth2Client } from "@/utils/gmail/client"; import { GOOGLE_LINKING_STATE_COOKIE_NAME } from "@/utils/gmail/constants"; @@ -11,18 +10,10 @@ import { export type GetAuthLinkUrlResponse = { url: string }; -const actionSchema = z.enum(["auto", "merge_confirmed"]); - -const getAuthUrl = ({ - userId, - action, -}: { - userId: string; - action: "auto" | "merge_confirmed"; -}) => { +const getAuthUrl = ({ userId }: { userId: string }) => { const googleAuth = getLinkingOAuth2Client(); - const state = generateOAuthState({ userId, action }); + const state = generateOAuthState({ userId }); const url = googleAuth.generateAuthUrl({ access_type: "offline", @@ -36,23 +27,7 @@ const getAuthUrl = ({ export const GET = withAuth("google/linking/auth-url", async (request) => { const userId = request.auth.userId; - const url = new URL(request.url); - - const actionParam = url.searchParams.get("action"); - const parseResult = actionSchema.safeParse(actionParam); - - if (!parseResult.success) { - return NextResponse.json( - { - error: - "Invalid or missing action parameter. Must be 'auto' or 'merge_confirmed'.", - }, - { status: 400 }, - ); - } - - const action = parseResult.data; - const { url: authUrl, state } = getAuthUrl({ userId, action }); + const { url: authUrl, state } = getAuthUrl({ userId }); const response = NextResponse.json({ url: authUrl }); diff --git a/apps/web/app/api/google/linking/callback/route.ts b/apps/web/app/api/google/linking/callback/route.ts index 3aa3470146..fba1e8fe44 100644 --- a/apps/web/app/api/google/linking/callback/route.ts +++ b/apps/web/app/api/google/linking/callback/route.ts @@ -30,7 +30,7 @@ export const GET = withError("google/linking/callback", async (request) => { return validation.response; } - const { targetUserId, action, code } = validation; + const { targetUserId, code } = validation; const redirectUrl = new URL("/accounts", request.nextUrl.origin); const response = NextResponse.redirect(redirectUrl); response.cookies.delete(GOOGLE_LINKING_STATE_COOKIE_NAME); @@ -95,7 +95,6 @@ export const GET = withError("google/linking/callback", async (request) => { hasEmailAccount: !!existingAccount?.emailAccount, existingUserId: existingAccount?.userId || null, targetUserId, - action, provider: "google", providerEmail, baseUrl: request.nextUrl.origin, diff --git a/apps/web/app/api/outlook/linking/auth-url/route.ts b/apps/web/app/api/outlook/linking/auth-url/route.ts index c179e4fdb3..86e3f8ffdc 100644 --- a/apps/web/app/api/outlook/linking/auth-url/route.ts +++ b/apps/web/app/api/outlook/linking/auth-url/route.ts @@ -1,5 +1,4 @@ import { NextResponse } from "next/server"; -import { z } from "zod"; import { withAuth } from "@/utils/middleware"; import { getLinkingOAuth2Url } from "@/utils/outlook/client"; import { OUTLOOK_LINKING_STATE_COOKIE_NAME } from "@/utils/outlook/constants"; @@ -10,16 +9,8 @@ import { export type GetOutlookAuthLinkUrlResponse = { url: string }; -const actionSchema = z.enum(["auto", "merge_confirmed"]); - -const getAuthUrl = ({ - userId, - action, -}: { - userId: string; - action: "auto" | "merge_confirmed"; -}) => { - const state = generateOAuthState({ userId, action }); +const getAuthUrl = ({ userId }: { userId: string }) => { + const state = generateOAuthState({ userId }); const baseUrl = getLinkingOAuth2Url(); const url = `${baseUrl}&state=${state}`; @@ -29,23 +20,7 @@ const getAuthUrl = ({ export const GET = withAuth("outlook/linking/auth-url", async (request) => { const userId = request.auth.userId; - const url = new URL(request.url); - - const actionParam = url.searchParams.get("action"); - const parseResult = actionSchema.safeParse(actionParam); - - if (!parseResult.success) { - return NextResponse.json( - { - error: - "Invalid or missing action parameter. Must be 'auto' or 'merge_confirmed'.", - }, - { status: 400 }, - ); - } - - const action = parseResult.data; - const { url: authUrl, state } = getAuthUrl({ userId, action }); + const { url: authUrl, state } = getAuthUrl({ userId }); const response = NextResponse.json({ url: authUrl }); diff --git a/apps/web/app/api/outlook/linking/callback/route.ts b/apps/web/app/api/outlook/linking/callback/route.ts index b1c4f94526..45c3172a33 100644 --- a/apps/web/app/api/outlook/linking/callback/route.ts +++ b/apps/web/app/api/outlook/linking/callback/route.ts @@ -33,7 +33,7 @@ export const GET = withError("outlook/linking/callback", async (request) => { return validation.response; } - const { targetUserId, action, code } = validation; + const { targetUserId, code } = validation; const redirectUrl = new URL("/accounts", request.nextUrl.origin); const response = NextResponse.redirect(redirectUrl); response.cookies.delete(OUTLOOK_LINKING_STATE_COOKIE_NAME); @@ -129,7 +129,6 @@ export const GET = withError("outlook/linking/callback", async (request) => { hasEmailAccount: !!existingAccount?.emailAccount, existingUserId: existingAccount?.userId || null, targetUserId, - action, provider: "microsoft", providerEmail, baseUrl: request.nextUrl.origin, diff --git a/apps/web/components/PremiumExpiredCard.tsx b/apps/web/components/PremiumCard.tsx similarity index 82% rename from apps/web/components/PremiumExpiredCard.tsx rename to apps/web/components/PremiumCard.tsx index 4ba71b0163..f3a9eadd32 100644 --- a/apps/web/components/PremiumExpiredCard.tsx +++ b/apps/web/components/PremiumCard.tsx @@ -28,11 +28,8 @@ export function PremiumExpiredCardContent({ onDismiss, isCollapsed = false, }: PremiumExpiredCardProps & { isCollapsed?: boolean }) { - // Early return if no premium data - if (!premium) return null; - // Convert string dates to Date objects if needed - const lemonSqueezyRenewsAt = premium.lemonSqueezyRenewsAt + const lemonSqueezyRenewsAt = premium?.lemonSqueezyRenewsAt ? typeof premium.lemonSqueezyRenewsAt === "string" ? new Date(premium.lemonSqueezyRenewsAt) : premium.lemonSqueezyRenewsAt @@ -40,17 +37,38 @@ export function PremiumExpiredCardContent({ const isUserPremium = isPremium( lemonSqueezyRenewsAt, - premium.stripeSubscriptionStatus || null, + premium?.stripeSubscriptionStatus || null, ); if (isUserPremium) return null; - // Determine the message based on subscription state const getSubscriptionMessage = () => { + const UPGRADE_MESSAGE = { + title: "Upgrade to Premium", + description: "Upgrade to Premium to enable your AI email assistant.", + }; + + if (!premium) { + return UPGRADE_MESSAGE; + } + const status = premium.stripeSubscriptionStatus; const hasLemonSqueezyExpired = lemonSqueezyRenewsAt && lemonSqueezyRenewsAt < new Date(); + // Check if user never had a subscription + const hasNoSubscription = + !status && + !premium.stripeSubscriptionId && + !premium.lemonSqueezySubscriptionId; + + if (!premium || hasNoSubscription) { + return { + title: "Upgrade to Premium", + description: "Upgrade to Premium to enable your AI email assistant.", + }; + } + if (status === "past_due") { return { title: "Payment Past Due", @@ -95,6 +113,15 @@ export function PremiumExpiredCardContent({ const { title, description } = getSubscriptionMessage(); + const isNewUser = + !premium || + (!premium.stripeSubscriptionStatus && + !premium.stripeSubscriptionId && + !premium.lemonSqueezySubscriptionId); + + const buttonText = isNewUser ? "Upgrade" : "Reactivate"; + const buttonHref = isNewUser ? "/premium" : "/settings"; + // When collapsed, show only the alert icon with a hover card if (isCollapsed) { return ( @@ -112,18 +139,18 @@ export function PremiumExpiredCardContent({ className="w-full bg-orange-600 text-white hover:bg-orange-700 border-0 shadow-sm h-8 mt-2" > - Reactivate + {buttonText}
} > @@ -170,11 +197,11 @@ export function PremiumExpiredCardContent({ className="w-full bg-orange-600 text-white hover:bg-orange-700 border-0 shadow-sm h-8" > - Reactivate + {buttonText} @@ -183,7 +210,7 @@ export function PremiumExpiredCardContent({ ); } -export function PremiumExpiredCard({ +export function PremiumCard({ isCollapsed = false, }: { isCollapsed?: boolean; diff --git a/apps/web/components/ReferralDialog.tsx b/apps/web/components/ReferralDialog.tsx index 60645fcf43..cc92ade1d0 100644 --- a/apps/web/components/ReferralDialog.tsx +++ b/apps/web/components/ReferralDialog.tsx @@ -28,7 +28,7 @@ export function ReferralDialog() { Refer friend - + @@ -96,12 +96,12 @@ export function Referrals() { } return ( -
+
-

+

Refer Friends, Get Rewards

-

+

Share Inbox Zero with friends and get a free month for each friend who completes their trial

@@ -118,21 +118,15 @@ export function Referrals() { {codeData?.code ? (
-
- - {link} - - {/* */} +
+
+ + {link} + +
-
+