diff --git a/apps/web/components/add-document/connections.tsx b/apps/web/components/add-document/connections.tsx index 7aa0250d5..221a73296 100644 --- a/apps/web/components/add-document/connections.tsx +++ b/apps/web/components/add-document/connections.tsx @@ -4,7 +4,7 @@ import { $fetch } from "@lib/api" import { hasActivePlan } from "@lib/queries" import type { ConnectionResponseSchema } from "@repo/validation/api" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" -import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons" +import { GoogleDrive, Granola, Notion, OneDrive } from "@ui/assets/icons" import { useCustomer } from "autumn-js/react" import { Check, @@ -31,12 +31,16 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "@ui/components/dropdown-menu" +import { GranolaConnectModal } from "@/components/granola-connect-modal" import { RemoveConnectionDialog } from "@/components/remove-connection-dialog" import { SyncStatusBadge } from "@/components/settings/sync-status-badge" import { SyncHistoryPanel } from "@/components/settings/sync-history-panel" import { useConnectionHealth } from "@/hooks/use-connection-health" import { useTriggerSync } from "@/hooks/use-trigger-sync" -import { formatRelativeTime } from "@/components/settings/sync-utils" +import { + formatRelativeTime, + getConnectionSubtitle, +} from "@/components/settings/sync-utils" import type { ImportProvider } from "@/components/settings/sync-utils" type GDriveSyncScope = "scoped" | "full" @@ -48,7 +52,7 @@ const GDRIVE_SCOPE_LABELS: Record = { type Connection = z.infer -type ConnectorProvider = "google-drive" | "notion" | "onedrive" +type ConnectorProvider = "google-drive" | "notion" | "onedrive" | "granola" const CONNECTORS: Record< ConnectorProvider, @@ -77,6 +81,12 @@ const CONNECTORS: Record< documentLabel: "documents", icon: OneDrive, }, + granola: { + title: "Granola", + description: "Sync AI meeting notes and transcripts", + documentLabel: "notes", + icon: Granola, + }, } as const /** Extract typed metadata from a connection, with runtime validation. */ @@ -162,7 +172,7 @@ function ConnectionRow({ "truncate text-[14px] text-[#737373]", )} > - {connection.email || "Unknown"} + {getConnectionSubtitle(connection)}
@@ -300,8 +310,12 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) { const queryClient = useQueryClient() const autumn = useCustomer() const isProUser = hasActivePlan(autumn.data?.subscriptions, "api_pro") + // Granola is a Max-tier (and above) connector, like the Gmail connector. + // Lower plans see it but can't connect — the API-key modal stays gated. + const isMaxUser = hasActivePlan(autumn.data?.subscriptions, "api_max") const [connectingProvider, setConnectingProvider] = useState(null) + const [granolaModalOpen, setGranolaModalOpen] = useState(false) const [gdriveSyncScope, setGdriveSyncScope] = useState("scoped") const [isUpgrading, setIsUpgrading] = useState(false) @@ -592,6 +606,21 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
+ ) : provider === "granola" ? ( + <> + {!isMaxUser && ( + + Max + + )} + + ) : ( + + + + + ) +} diff --git a/apps/web/components/settings/connections-mcp.tsx b/apps/web/components/settings/connections-mcp.tsx index 26412728f..08e79fa32 100644 --- a/apps/web/components/settings/connections-mcp.tsx +++ b/apps/web/components/settings/connections-mcp.tsx @@ -4,7 +4,7 @@ import { dmSans125ClassName } from "@/lib/fonts" import { cn } from "@lib/utils" import { $fetch } from "@lib/api" import { hasActivePlan } from "@lib/queries" -import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons" +import { GoogleDrive, Granola, Notion, OneDrive } from "@ui/assets/icons" import { useCustomer } from "autumn-js/react" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { @@ -33,7 +33,10 @@ import { SyncStatusBadge } from "@/components/settings/sync-status-badge" import { SyncHistoryPanel } from "@/components/settings/sync-history-panel" import { useConnectionHealth } from "@/hooks/use-connection-health" import { useTriggerSync } from "@/hooks/use-trigger-sync" -import { formatRelativeTime } from "@/components/settings/sync-utils" +import { + formatRelativeTime, + getConnectionSubtitle, +} from "@/components/settings/sync-utils" import type { ImportProvider } from "@/components/settings/sync-utils" type Connection = z.infer @@ -68,6 +71,12 @@ const CONNECTORS = { icon: OneDrive, documentLabel: "documents", }, + granola: { + title: "Granola", + description: "Sync AI meeting notes and transcripts", + icon: Granola, + documentLabel: "notes", + }, } as const type ConnectorProvider = keyof typeof CONNECTORS @@ -231,7 +240,7 @@ function ConnectionRow({ "font-medium text-[16px] tracking-[-0.16px] text-[#737373]", )} > - {connection.email || "Unknown"} + {getConnectionSubtitle(connection)}
diff --git a/apps/web/components/settings/sync-utils.ts b/apps/web/components/settings/sync-utils.ts index 25a683ba8..7028b545d 100644 --- a/apps/web/components/settings/sync-utils.ts +++ b/apps/web/components/settings/sync-utils.ts @@ -42,9 +42,17 @@ export const PROVIDER_DISPLAY_NAMES: Record = { github: "GitHub", "web-crawler": "Web Crawler", s3: "S3", + granola: "Granola", +} + +export function getConnectionSubtitle(conn: { + provider: string + email?: string | null +}): string { + if (conn.provider === "granola") return "Granola workspace" + return conn.email || "Unknown" } -/** Provider type union matching the backend import endpoint */ export type ImportProvider = | "google-drive" | "notion" @@ -53,3 +61,4 @@ export type ImportProvider = | "github" | "web-crawler" | "s3" + | "granola" diff --git a/packages/lib/api.ts b/packages/lib/api.ts index 4ce787f23..7d33b1307 100644 --- a/packages/lib/api.ts +++ b/packages/lib/api.ts @@ -84,13 +84,15 @@ export const apiSchema = createSchema({ redirectUrl: z.string().optional(), }), output: z.object({ - authLink: z.string(), - expiresIn: z.string(), + // authLink/expiresIn are present for OAuth providers (Drive/Notion/OneDrive) + // but absent for credential-based ones like Granola where there's no redirect. + authLink: z.string().optional(), + expiresIn: z.string().optional(), id: z.string(), redirectsTo: z.string().optional(), }), params: z.object({ - provider: z.enum(["google-drive", "notion", "onedrive"]), + provider: z.enum(["google-drive", "notion", "onedrive", "granola"]), }), }, @@ -158,6 +160,7 @@ export const apiSchema = createSchema({ "github", "web-crawler", "s3", + "granola", ]), }), }, diff --git a/packages/ui/assets/icons.tsx b/packages/ui/assets/icons.tsx index 9c30d2e3e..0b7b7d6fc 100644 --- a/packages/ui/assets/icons.tsx +++ b/packages/ui/assets/icons.tsx @@ -362,3 +362,18 @@ export const ClaudeDesktopIcon = ({ className }: { className?: string }) => { ) } + +export const Granola = ({ className }: { className?: string }) => ( + + Granola + + +)