diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx index c2e49697e2..55c55ade8d 100644 --- a/apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx +++ b/apps/web/app/(app)/[emailAccountId]/assistant/Rules.tsx @@ -52,7 +52,6 @@ import { sortActionsByPriority } from "@/utils/action-sort"; import { getActionDisplay, getActionIcon } from "@/utils/action-display"; import { RuleDialog } from "./RuleDialog"; import { useDialogState } from "@/hooks/useDialogState"; -import { ColdEmailDialog } from "@/app/(app)/[emailAccountId]/cold-email-blocker/ColdEmailDialog"; import { useChat } from "@/providers/ChatProvider"; import { useSidebar } from "@/components/ui/sidebar"; import { useLabels } from "@/hooks/useLabels"; @@ -63,6 +62,10 @@ import { getDefaultActions, } from "@/utils/rule/consts"; import { DEFAULT_COLD_EMAIL_PROMPT } from "@/utils/cold-email/prompt"; +import { + STEP_KEYS, + getStepNumber, +} from "@/app/(app)/[emailAccountId]/onboarding/OnboardingContent"; export function Rules({ showAddRuleButton = true, @@ -79,8 +82,6 @@ export function Rules({ editMode?: boolean; duplicateRule?: RulesResponse[number]; }>(); - const coldEmailDialog = useDialogState(); - const onCreateRule = () => ruleDialog.onOpen(); const { emailAccountId, provider } = useAccount(); @@ -298,14 +299,10 @@ export function Rules({ > { - if (isColdEmailBlocker) { - coldEmailDialog.onOpen(); - } else { - ruleDialog.onOpen({ - ruleId: rule.id, - editMode: true, - }); - } + ruleDialog.onOpen({ + ruleId: rule.id, + editMode: true, + }); }} > @@ -423,11 +420,6 @@ export function Rules({ }} editMode={ruleDialog.data?.editMode} /> - - ); } @@ -478,7 +470,12 @@ function NoRules() { You don't have any rules yet.
diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/onboarding/CategoriesSetup.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/onboarding/CategoriesSetup.tsx deleted file mode 100644 index 0f8e52905f..0000000000 --- a/apps/web/app/(app)/[emailAccountId]/assistant/onboarding/CategoriesSetup.tsx +++ /dev/null @@ -1,260 +0,0 @@ -"use client"; - -import { useCallback, useState } from "react"; -import { useRouter } from "next/navigation"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; -import { TypographyH3, TypographyP } from "@/components/Typography"; -import { Card, CardContent } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; -import { createRulesOnboardingAction } from "@/utils/actions/rule"; -import { - createRulesOnboardingBody, - type CreateRulesOnboardingBody, - type CategoryAction, -} from "@/utils/actions/rule.validation"; -import { TooltipExplanation } from "@/components/TooltipExplanation"; -import { - ASSISTANT_ONBOARDING_COOKIE, - markOnboardingAsCompleted, -} from "@/utils/cookies"; -import { prefixPath } from "@/utils/path"; -import Image from "next/image"; -import { - ExampleDialog, - SeeExampleDialogButton, -} from "@/app/(app)/[emailAccountId]/assistant/onboarding/ExampleDialog"; -import { categoryConfig } from "@/utils/category-config"; -import { useAccount } from "@/providers/EmailAccountProvider"; -import { - type IconCircleColor, - textVariants, -} from "@/app/(app)/[emailAccountId]/onboarding/IconCircle"; -import { cn } from "@/utils"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { - isGoogleProvider, - isMicrosoftProvider, -} from "@/utils/email/provider-types"; - -const NEXT_URL = "/assistant/onboarding/draft-replies"; - -export function CategoriesSetup({ - defaultValues, -}: { - defaultValues: CreateRulesOnboardingBody; -}) { - const router = useRouter(); - const { emailAccountId, provider } = useAccount(); - - const [showExampleDialog, setShowExampleDialog] = useState(false); - - const normalizeAction = ( - action: CategoryAction | null | undefined, - ): CategoryAction | null | undefined => { - if (!action) return action; - // Remapping Google archive variants to move-folder variants (if they exist in the db already) - if (isMicrosoftProvider(provider)) { - if (action === "label_archive") return "move_folder"; - if (action === "label_archive_delayed") return "move_folder_delayed"; - } - return action; - }; - - const initialCategories = categoryConfig(provider).map((config) => { - const existingValue = defaultValues.find( - (val) => val.name === config.label, - ); - - return { - key: config.key, - name: config.label, - description: existingValue?.description || "", - action: normalizeAction(existingValue?.action || config.action), - hasDigest: existingValue?.hasDigest, - }; - }); - - const form = useForm<{ categories: CreateRulesOnboardingBody }>({ - resolver: zodResolver(z.object({ categories: createRulesOnboardingBody })), - defaultValues: { categories: initialCategories }, - }); - - const onSubmit = useCallback( - async (data: { categories: CreateRulesOnboardingBody }) => { - // runs in background so we can move on to next step faster - createRulesOnboardingAction(emailAccountId, data.categories); - router.push(prefixPath(emailAccountId, NEXT_URL)); - }, - [emailAccountId, router], - ); - - return ( -
- - - How do you want your emails organized? - - - - We'll automatically categorize your emails to help you focus on what - matters. -
- You can add custom categories and rules later.{" "} - setShowExampleDialog(true)} /> -
- - - } - /> - -
- {categoryConfig(provider).map((category, index) => ( - - ))} -
- -
- - - -
- - - ); -} - -function CategoryCard({ - index, - label, - Icon, - iconColor, - form, - tooltipText, - categoryName, - provider, -}: { - index: number; - label: string; - Icon: React.ElementType; - iconColor: IconCircleColor; - form: ReturnType>; - tooltipText?: string; - categoryName: string; - provider: string; -}) { - return ( - - - -
- {label} - {tooltipText && ( - - )} -
-
- { - return ( - - - - ); - }} - /> -
-
-
- ); -} diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/onboarding/ExampleDialog.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/onboarding/ExampleDialog.tsx deleted file mode 100644 index 2c5a73e4ed..0000000000 --- a/apps/web/app/(app)/[emailAccountId]/assistant/onboarding/ExampleDialog.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; - -export const ExampleDialog = ({ - open, - onOpenChange, - title, - description, - image, -}: { - open: boolean; - onOpenChange: (open: boolean) => void; - title: string; - description: string; - image: React.ReactNode; -}) => { - return ( - - - - {title} - {description} - - {image} - - - ); -}; - -export function SeeExampleDialogButton({ onClick }: { onClick: () => void }) { - return ( - - ); -} diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/onboarding/completed/page.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/onboarding/completed/page.tsx deleted file mode 100644 index 236c9749f5..0000000000 --- a/apps/web/app/(app)/[emailAccountId]/assistant/onboarding/completed/page.tsx +++ /dev/null @@ -1,47 +0,0 @@ -"use client"; - -import { use } from "react"; -import Link from "next/link"; -import { Check } from "lucide-react"; -import { TypographyH3, TypographyP } from "@/components/Typography"; -import { Card } from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { prefixPath } from "@/utils/path"; - -export default function CompletedPage(props: { - params: Promise<{ emailAccountId: string }>; -}) { - const { emailAccountId } = use(props.params); - return ( -
- -
-
- -
- - You're all set! - -
- - Your emails will be automatically categorized. - - - - Want to customize further? You can update your rules on the - Assistant page and fine-tune your preferences anytime. - -
- -
- -
-
-
-
- ); -} diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/onboarding/draft-replies/page.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/onboarding/draft-replies/page.tsx deleted file mode 100644 index a498d7264c..0000000000 --- a/apps/web/app/(app)/[emailAccountId]/assistant/onboarding/draft-replies/page.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client"; - -import { useCallback } from "react"; -import { useRouter } from "next/navigation"; -import { Card } from "@/components/ui/card"; -import { TypographyH3, TypographyP } from "@/components/Typography"; -import { ButtonListSurvey } from "@/components/ButtonListSurvey"; -import { enableDraftRepliesAction } from "@/utils/actions/rule"; -import { toastError } from "@/components/Toast"; -import { useAccount } from "@/providers/EmailAccountProvider"; -import { prefixPath } from "@/utils/path"; - -export default function DraftRepliesPage() { - const router = useRouter(); - const { emailAccountId } = useAccount(); - - const onSetDraftReplies = useCallback( - async (value: string) => { - const result = await enableDraftRepliesAction(emailAccountId, { - enable: value === "yes", - }); - - if (result?.serverError) { - toastError({ - description: `There was an error: ${result.serverError || ""}`, - }); - } - - router.push( - prefixPath(emailAccountId, "/assistant/onboarding/completed"), - ); - }, - [router, emailAccountId], - ); - - return ( -
- -
- - Would you like our AI to automatically draft replies for you? - - - - The drafts will appear in your inbox, written in your tone and - style. You can edit them before sending. - - - -
-
-
- ); -} diff --git a/apps/web/app/(app)/[emailAccountId]/assistant/onboarding/page.tsx b/apps/web/app/(app)/[emailAccountId]/assistant/onboarding/page.tsx deleted file mode 100644 index c1100d8c8e..0000000000 --- a/apps/web/app/(app)/[emailAccountId]/assistant/onboarding/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; - -import useSWR from "swr"; -import { Card } from "@/components/ui/card"; -import { CategoriesSetup } from "./CategoriesSetup"; -import type { GetCategorizationPreferencesResponse } from "@/app/api/user/categorization-preferences/route"; -import { LoadingContent } from "@/components/LoadingContent"; - -export default function OnboardingPage() { - const { - data: defaultValues, - isLoading, - error, - } = useSWR( - "/api/user/categorization-preferences", - ); - - return ( - - - - - - ); -} diff --git a/apps/web/app/(app)/[emailAccountId]/automation/page.tsx b/apps/web/app/(app)/[emailAccountId]/automation/page.tsx index ec98251c50..b250926cff 100644 --- a/apps/web/app/(app)/[emailAccountId]/automation/page.tsx +++ b/apps/web/app/(app)/[emailAccountId]/automation/page.tsx @@ -18,6 +18,10 @@ import { AIChatButton } from "@/app/(app)/[emailAccountId]/assistant/AIChatButto import { PageWrapper } from "@/components/PageWrapper"; import { PageHeader } from "@/components/PageHeader"; import { DismissibleVideoCard } from "@/components/VideoCard"; +import { + STEP_KEYS, + getStepNumber, +} from "@/app/(app)/[emailAccountId]/onboarding/OnboardingContent"; export const maxDuration = 300; // Applies to the actions @@ -67,7 +71,12 @@ export default async function AutomationPage({ }); if (!hasRule) { - redirect(prefixPath(emailAccountId, "/assistant/onboarding")); + redirect( + prefixPath( + emailAccountId, + `/onboarding?step=${getStepNumber(STEP_KEYS.LABELS)}`, + ), + ); } } diff --git a/apps/web/app/(app)/[emailAccountId]/cold-email-blocker/ColdEmailContent.tsx b/apps/web/app/(app)/[emailAccountId]/cold-email-blocker/ColdEmailContent.tsx index e386a190af..eadb9317e6 100644 --- a/apps/web/app/(app)/[emailAccountId]/cold-email-blocker/ColdEmailContent.tsx +++ b/apps/web/app/(app)/[emailAccountId]/cold-email-blocker/ColdEmailContent.tsx @@ -9,12 +9,13 @@ import { Button } from "@/components/ui/button"; import { prefixPath } from "@/utils/path"; import { useAccount } from "@/providers/EmailAccountProvider"; import Link from "next/link"; +import { MessageText } from "@/components/Typography"; export function ColdEmailContent({ searchParam }: { searchParam?: string }) { const { emailAccountId } = useAccount(); return ( - + Test Cold Emails @@ -38,9 +39,13 @@ export function ColdEmailContent({ searchParam }: { searchParam?: string }) { - diff --git a/apps/web/app/(app)/[emailAccountId]/cold-email-blocker/ColdEmailDialog.tsx b/apps/web/app/(app)/[emailAccountId]/cold-email-blocker/ColdEmailDialog.tsx deleted file mode 100644 index 014212e048..0000000000 --- a/apps/web/app/(app)/[emailAccountId]/cold-email-blocker/ColdEmailDialog.tsx +++ /dev/null @@ -1,29 +0,0 @@ -"use client"; - -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { ColdEmailContent } from "./ColdEmailContent"; - -interface ColdEmailDialogProps { - isOpen: boolean; - onClose: () => void; - onSuccess?: () => void; -} - -export function ColdEmailDialog({ isOpen, onClose }: ColdEmailDialogProps) { - return ( - - - - Cold Email Blocker - - - - - - ); -} diff --git a/apps/web/app/(app)/[emailAccountId]/cold-email-blocker/page.tsx b/apps/web/app/(app)/[emailAccountId]/cold-email-blocker/page.tsx index 09d2c65d16..525fa80185 100644 --- a/apps/web/app/(app)/[emailAccountId]/cold-email-blocker/page.tsx +++ b/apps/web/app/(app)/[emailAccountId]/cold-email-blocker/page.tsx @@ -8,10 +8,7 @@ import { PageHeader } from "@/components/PageHeader"; export default function ColdEmailBlockerPage() { return ( - + diff --git a/apps/web/app/(app)/[emailAccountId]/onboarding/OnboardingContent.tsx b/apps/web/app/(app)/[emailAccountId]/onboarding/OnboardingContent.tsx index d76071e537..c308e2241f 100644 --- a/apps/web/app/(app)/[emailAccountId]/onboarding/OnboardingContent.tsx +++ b/apps/web/app/(app)/[emailAccountId]/onboarding/OnboardingContent.tsx @@ -23,6 +23,33 @@ import { isDefined } from "@/utils/types"; import { StepCompanySize } from "@/app/(app)/[emailAccountId]/onboarding/StepCompanySize"; import { usePremium } from "@/components/PremiumAlert"; +export const STEP_KEYS = { + INTRO: "intro", + FEATURES: "features", + WHO: "who", + COMPANY_SIZE: "companySize", + LABELS: "labels", + DRAFT: "draft", + CUSTOM_RULES: "customRules", +} as const; + +const STEP_ORDER = [ + STEP_KEYS.INTRO, + STEP_KEYS.FEATURES, + STEP_KEYS.WHO, + STEP_KEYS.COMPANY_SIZE, + STEP_KEYS.LABELS, + STEP_KEYS.DRAFT, + STEP_KEYS.CUSTOM_RULES, +] as const; + +export function getStepNumber( + stepKey: (typeof STEP_KEYS)[keyof typeof STEP_KEYS], +): number { + const index = STEP_ORDER.indexOf(stepKey); + return index === -1 ? 1 : index + 1; +} + interface OnboardingContentProps { step: number; } @@ -33,34 +60,37 @@ export function OnboardingContent({ step }: OnboardingContentProps) { useSignUpEvent(); - const steps = [ - () => , - () => , - () => ( + const stepMap = { + [STEP_KEYS.INTRO]: () => , + [STEP_KEYS.FEATURES]: () => , + [STEP_KEYS.WHO]: () => ( ), - () => , - () => ( + [STEP_KEYS.COMPANY_SIZE]: () => , + [STEP_KEYS.LABELS]: () => ( ), - () => ( + [STEP_KEYS.DRAFT]: () => ( ), - // - () => , - ].filter(isDefined); + [STEP_KEYS.CUSTOM_RULES]: () => ( + + ), + }; + + const steps = STEP_ORDER.map((key) => stepMap[key]).filter(isDefined); const { data, mutate } = usePersona(); const clampedStep = Math.min(Math.max(step, 1), steps.length); diff --git a/apps/web/app/(app)/[emailAccountId]/setup/SetupContent.tsx b/apps/web/app/(app)/[emailAccountId]/setup/SetupContent.tsx index 0c06ff7686..e3f33c7900 100644 --- a/apps/web/app/(app)/[emailAccountId]/setup/SetupContent.tsx +++ b/apps/web/app/(app)/[emailAccountId]/setup/SetupContent.tsx @@ -20,6 +20,10 @@ import { LoadingContent } from "@/components/LoadingContent"; import { EXTENSION_URL } from "@/utils/config"; import { isGoogleProvider } from "@/utils/email/provider-types"; import { useAccount } from "@/providers/EmailAccountProvider"; +import { + STEP_KEYS, + getStepNumber, +} from "@/app/(app)/[emailAccountId]/onboarding/OnboardingContent"; function FeatureCard({ emailAccountId, @@ -170,12 +174,14 @@ const StepItem = ({
{completed ? ( -
- -
+ +
+ +
+ ) : ( <> {showMarkDone && ( @@ -209,7 +215,6 @@ function Checklist({ completedCount, totalSteps, progressPercentage, - isReplyTrackerConfigured, isBulkUnsubscribeConfigured, isAiAssistantConfigured, isCalendarConnected, @@ -219,7 +224,6 @@ function Checklist({ completedCount: number; totalSteps: number; progressPercentage: number; - isReplyTrackerConfigured: boolean; isBulkUnsubscribeConfigured: boolean; isAiAssistantConfigured: boolean; isCalendarConnected: boolean; @@ -253,7 +257,10 @@ function Checklist({
} iconBg="bg-green-100 dark:bg-green-900/50" iconColor="text-green-500 dark:text-green-400" @@ -274,22 +281,11 @@ function Checklist({ actionText="View" /> - } - iconBg="bg-blue-100 dark:bg-blue-900/50" - iconColor="text-blue-500 dark:text-blue-400" - title="View emails needing replies" - timeEstimate="30 seconds" - completed={isReplyTrackerConfigured} - actionText="View" - /> - } - iconBg="bg-yellow-100 dark:bg-yellow-900/50" - iconColor="text-yellow-600 dark:text-yellow-400" + iconBg="bg-blue-100 dark:bg-blue-900/50" + iconColor="text-blue-500 dark:text-blue-400" title="Connect your calendar" timeEstimate="2 minutes" completed={isCalendarConnected} @@ -325,7 +321,6 @@ export function SetupContent() { ; - -export type GetCategorizationPreferencesResponse = Awaited< - ReturnType ->; - -export const GET = withEmailProvider( - "user/categorization-preferences", - async (request) => { - const emailAccountId = request.auth.emailAccountId; - const result = await getUserPreferences({ emailAccountId }); - return NextResponse.json(result); - }, -); - -async function getUserPreferences({ - emailAccountId, -}: { - emailAccountId: string; -}): Promise { - const emailAccount = await prisma.emailAccount.findUnique({ - where: { id: emailAccountId }, - select: { - rules: { - select: { - systemType: true, - actions: { - select: { - type: true, - }, - }, - }, - }, - }, - }); - if (!emailAccount) return []; - - return [ - { - name: getRuleName(SystemType.TO_REPLY), - key: SystemType.TO_REPLY, - description: "", - ...getRuleSetting(SystemType.TO_REPLY, emailAccount.rules), - }, - { - name: getRuleName(SystemType.COLD_EMAIL), - key: SystemType.COLD_EMAIL, - description: "", - ...getRuleSetting(SystemType.COLD_EMAIL, emailAccount.rules), - }, - { - name: getRuleName(SystemType.NEWSLETTER), - key: SystemType.NEWSLETTER, - description: "", - ...getRuleSetting(SystemType.NEWSLETTER, emailAccount.rules), - }, - { - name: getRuleName(SystemType.MARKETING), - key: SystemType.MARKETING, - description: "", - ...getRuleSetting(SystemType.MARKETING, emailAccount.rules), - }, - { - name: getRuleName(SystemType.CALENDAR), - key: SystemType.CALENDAR, - description: "", - ...getRuleSetting(SystemType.CALENDAR, emailAccount.rules), - }, - { - name: getRuleName(SystemType.RECEIPT), - key: SystemType.RECEIPT, - description: "", - ...getRuleSetting(SystemType.RECEIPT, emailAccount.rules), - }, - { - name: getRuleName(SystemType.NOTIFICATION), - key: SystemType.NOTIFICATION, - description: "", - ...getRuleSetting(SystemType.NOTIFICATION, emailAccount.rules), - }, - ]; -} - -function getRuleSetting( - systemType: SystemType, - rules?: EmailAccountPreferences["rules"], -): CategoryConfig | undefined { - const rule = rules?.find((rule) => rule.systemType === systemType); - const hasDigest = rule?.actions.some( - (action) => action.type === ActionType.DIGEST, - ); - if (!rule) return undefined; - - if (rule.actions.some((action) => action.type === ActionType.MOVE_FOLDER)) - return { action: "move_folder", hasDigest }; - if (rule.actions.some((action) => action.type === ActionType.ARCHIVE)) - return { action: "label_archive", hasDigest }; - if (rule.actions.some((action) => action.type === ActionType.LABEL)) - return { action: "label", hasDigest }; - return { action: "none", hasDigest }; -} diff --git a/apps/web/app/api/user/setup-progress/route.ts b/apps/web/app/api/user/setup-progress/route.ts index 1c45c48327..2ef9adef2a 100644 --- a/apps/web/app/api/user/setup-progress/route.ts +++ b/apps/web/app/api/user/setup-progress/route.ts @@ -1,8 +1,6 @@ import { NextResponse } from "next/server"; import prisma from "@/utils/prisma"; import { withEmailAccount } from "@/utils/middleware"; -import { cookies } from "next/headers"; -import { REPLY_ZERO_ONBOARDING_COOKIE } from "@/utils/cookies"; export type GetSetupProgressResponse = Awaited< ReturnType @@ -36,14 +34,9 @@ async function getSetupProgress({ throw new Error("Email account not found"); } - const cookieStore = await cookies(); - const isReplyTrackerConfigured = - cookieStore.get(REPLY_ZERO_ONBOARDING_COOKIE)?.value === "true"; - const steps = { aiAssistant: emailAccount.rules.length > 0, bulkUnsubscribe: emailAccount.newsletters.length > 0, - replyTracker: isReplyTrackerConfigured, calendarConnected: emailAccount.calendarConnections.length > 0, }; diff --git a/version.txt b/version.txt index 5380f623dd..52b9a1d4be 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v2.20.11 +v2.20.12