diff --git a/apps/web/components/menu.tsx b/apps/web/components/menu.tsx
index 9b9abd81..926b2a7a 100644
--- a/apps/web/components/menu.tsx
+++ b/apps/web/components/menu.tsx
@@ -9,7 +9,7 @@ import { Button } from "@repo/ui/components/button";
import { HeadingH2Bold } from "@repo/ui/text/heading/heading-h2-bold";
import { GlassMenuEffect } from "@ui/other/glass-effect";
import { useCustomer } from "autumn-js/react";
-import { MessageSquareMore, Plus, User, X } from "lucide-react";
+import { MessageSquareMore, Plus, Puzzle, User, X } from "lucide-react";
import { AnimatePresence, LayoutGroup, motion } from "motion/react";
import { useEffect, useState } from "react";
import { Drawer } from "vaul";
@@ -19,6 +19,7 @@ import { useChatOpen } from "@/stores";
import { ProjectSelector } from "./project-selector";
import { useTour } from "./tour";
import { AddMemoryExpandedView, AddMemoryView } from "./views/add-memory";
+import { IntegrationsView } from "./views/integrations";
import { MCPView } from "./views/mcp";
import { ProfileView } from "./views/profile";
@@ -41,7 +42,7 @@ const MCPIcon = ({ className }: { className?: string }) => {
function Menu({ id }: { id?: string }) {
const [isHovered, setIsHovered] = useState(false);
const [expandedView, setExpandedView] = useState<
- "addUrl" | "mcp" | "projects" | "profile" | null
+ "addUrl" | "mcp" | "projects" | "profile" | "integrations" | null
>(null);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isCollapsing, setIsCollapsing] = useState(false);
@@ -52,12 +53,12 @@ function Menu({ id }: { id?: string }) {
const autumn = useCustomer();
const { setIsOpen } = useChatOpen();
- const { data: memoriesCheck } = fetchMemoriesFeature(autumn as any);
+ const { data: memoriesCheck } = fetchMemoriesFeature(autumn);
const memoriesUsed = memoriesCheck?.usage ?? 0;
const memoriesLimit = memoriesCheck?.included_usage ?? 0;
- const { data: proCheck } = fetchConsumerProProduct(autumn as any);
+ const { data: proCheck } = fetchConsumerProProduct(autumn);
useEffect(() => {
if (memoriesCheck) {
@@ -79,6 +80,7 @@ function Menu({ id }: { id?: string }) {
addUrl: TOUR_STEP_IDS.MENU_ADD_MEMORY,
projects: TOUR_STEP_IDS.MENU_PROJECTS,
mcp: TOUR_STEP_IDS.MENU_MCP,
+ integrations: "", // No tour ID for integrations yet
};
const menuItems = [
@@ -94,6 +96,12 @@ function Menu({ id }: { id?: string }) {
key: "chat" as const,
disabled: false,
},
+ {
+ icon: Puzzle,
+ text: "Integrations",
+ key: "integrations" as const,
+ disabled: false,
+ },
{
icon: MCPIcon,
text: "MCP",
@@ -109,7 +117,7 @@ function Menu({ id }: { id?: string }) {
];
const handleMenuItemClick = (
- key: "chat" | "addUrl" | "mcp" | "projects" | "profile",
+ key: "chat" | "addUrl" | "mcp" | "projects" | "profile" | "integrations",
) => {
if (key === "chat") {
setIsOpen(true);
@@ -365,6 +373,7 @@ function Menu({ id }: { id?: string }) {
{expandedView === "mcp" && "Model Context Protocol"}
{expandedView === "profile" && "Profile"}
+ {expandedView === "integrations" && "Integrations"}
{expandedView === "mcp" && }
{expandedView === "profile" && }
+ {expandedView === "integrations" && (
+
+ )}
)}
@@ -485,7 +497,7 @@ function Menu({ id }: { id?: string }) {
{/* Menu content */}
-
+
@@ -590,6 +603,8 @@ function Menu({ id }: { id?: string }) {
{expandedView === "mcp" &&
"Model Context Protocol"}
{expandedView === "profile" && "Profile"}
+ {expandedView === "integrations" &&
+ "Integrations"}
}
{expandedView === "profile" && }
+ {expandedView === "integrations" && (
+
+ )}
diff --git a/apps/web/components/views/integrations.tsx b/apps/web/components/views/integrations.tsx
new file mode 100644
index 00000000..5174aeca
--- /dev/null
+++ b/apps/web/components/views/integrations.tsx
@@ -0,0 +1,669 @@
+import { $fetch } from "@lib/api";
+import { authClient } from "@lib/auth";
+import { useAuth } from "@lib/auth-context";
+import { generateId } from "@lib/generate-id";
+import {
+ ADD_MEMORY_SHORTCUT_URL,
+ SEARCH_MEMORY_SHORTCUT_URL,
+} from "@repo/lib/constants";
+import {
+ fetchConnectionsFeature,
+ fetchConsumerProProduct,
+} from "@repo/lib/queries";
+import { Button } from "@repo/ui/components/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogPortal,
+ DialogTitle,
+} from "@repo/ui/components/dialog";
+import { Skeleton } from "@repo/ui/components/skeleton";
+import type { ConnectionResponseSchema } from "@repo/validation/api";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons";
+import { useCustomer } from "autumn-js/react";
+import { Check, Copy, Smartphone, Trash2 } from "lucide-react";
+import { motion } from "motion/react";
+import Image from "next/image";
+import { useEffect, useId, useState } from "react";
+import { toast } from "sonner";
+import type { z } from "zod";
+import { analytics } from "@/lib/analytics";
+import { useProject } from "@/stores";
+
+type Connection = z.infer
;
+
+const CONNECTORS = {
+ "google-drive": {
+ title: "Google Drive",
+ description: "Connect your Google Docs, Sheets, and Slides",
+ icon: GoogleDrive,
+ },
+ notion: {
+ title: "Notion",
+ description: "Import your Notion pages and databases",
+ icon: Notion,
+ },
+ onedrive: {
+ title: "OneDrive",
+ description: "Access your Microsoft Office documents",
+ icon: OneDrive,
+ },
+} as const;
+
+type ConnectorProvider = keyof typeof CONNECTORS;
+
+const ChromeIcon = ({ className }: { className?: string }) => (
+
+);
+
+export function IntegrationsView() {
+ const { org } = useAuth();
+ const queryClient = useQueryClient();
+ const { selectedProject } = useProject();
+ const autumn = useCustomer();
+ const [showApiKeyModal, setShowApiKeyModal] = useState(false);
+ const [apiKey, setApiKey] = useState("");
+ const [copied, setCopied] = useState(false);
+ const [selectedShortcutType, setSelectedShortcutType] = useState<
+ "add" | "search" | null
+ >(null);
+ const apiKeyId = useId();
+
+ const handleUpgrade = async () => {
+ try {
+ await autumn.attach({
+ productId: "consumer_pro",
+ successUrl: "https://app.supermemory.ai/",
+ });
+ window.location.reload();
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ const { data: connectionsCheck } = fetchConnectionsFeature(autumn as any);
+ const connectionsUsed = connectionsCheck?.balance ?? 0;
+ const connectionsLimit = connectionsCheck?.included_usage ?? 0;
+
+ const { data: proCheck } = fetchConsumerProProduct(autumn as any);
+ const isProUser = proCheck?.allowed ?? false;
+
+ const canAddConnection = connectionsUsed < connectionsLimit;
+
+ const {
+ data: connections = [],
+ isLoading: connectionsLoading,
+ error: connectionsError,
+ } = useQuery({
+ queryKey: ["connections"],
+ queryFn: async () => {
+ const response = await $fetch("@post/connections/list", {
+ body: {
+ containerTags: [],
+ },
+ });
+
+ if (response.error) {
+ throw new Error(
+ response.error?.message || "Failed to load connections",
+ );
+ }
+
+ return response.data as Connection[];
+ },
+ staleTime: 30 * 1000,
+ refetchInterval: 60 * 1000,
+ });
+
+ useEffect(() => {
+ if (connectionsError) {
+ toast.error("Failed to load connections", {
+ description:
+ connectionsError instanceof Error
+ ? connectionsError.message
+ : "Unknown error",
+ });
+ }
+ }, [connectionsError]);
+
+ const addConnectionMutation = useMutation({
+ mutationFn: async (provider: ConnectorProvider) => {
+ if (!canAddConnection && !isProUser) {
+ throw new Error(
+ "Free plan doesn't include connections. Upgrade to Pro for unlimited connections.",
+ );
+ }
+
+ const response = await $fetch("@post/connections/:provider", {
+ params: { provider },
+ body: {
+ redirectUrl: window.location.href,
+ containerTags: [selectedProject],
+ },
+ });
+
+ // biome-ignore lint/style/noNonNullAssertion: its fine
+ if ("data" in response && !("error" in response.data!)) {
+ return response.data;
+ }
+
+ throw new Error(response.error?.message || "Failed to connect");
+ },
+ onSuccess: (data, provider) => {
+ analytics.connectionAdded(provider);
+ analytics.connectionAuthStarted();
+ if (data?.authLink) {
+ window.location.href = data.authLink;
+ }
+ },
+ onError: (error, provider) => {
+ analytics.connectionAuthFailed();
+ toast.error(`Failed to connect ${provider}`, {
+ description: error instanceof Error ? error.message : "Unknown error",
+ });
+ },
+ });
+
+ const deleteConnectionMutation = useMutation({
+ mutationFn: async (connectionId: string) => {
+ await $fetch(`@delete/connections/${connectionId}`);
+ },
+ onSuccess: () => {
+ analytics.connectionDeleted();
+ toast.success(
+ "Connection removal has started. supermemory will permanently delete all documents related to the connection in the next few minutes.",
+ );
+ queryClient.invalidateQueries({ queryKey: ["connections"] });
+ },
+ onError: (error) => {
+ toast.error("Failed to remove connection", {
+ description: error instanceof Error ? error.message : "Unknown error",
+ });
+ },
+ });
+
+ const createApiKeyMutation = useMutation({
+ mutationFn: async () => {
+ const res = await authClient.apiKey.create({
+ metadata: {
+ organizationId: org?.id,
+ type: "ios-shortcut",
+ },
+ name: `ios-${generateId().slice(0, 8)}`,
+ prefix: `sm_${org?.id}_`,
+ });
+ return res.key;
+ },
+ onSuccess: (apiKey) => {
+ setApiKey(apiKey);
+ setShowApiKeyModal(true);
+ setCopied(false);
+ handleCopyApiKey();
+ },
+ onError: (error) => {
+ toast.error("Failed to create API key", {
+ description: error instanceof Error ? error.message : "Unknown error",
+ });
+ },
+ });
+
+ const handleShortcutClick = (shortcutType: "add" | "search") => {
+ setSelectedShortcutType(shortcutType);
+ createApiKeyMutation.mutate();
+ };
+
+ const handleCopyApiKey = async () => {
+ try {
+ await navigator.clipboard.writeText(apiKey);
+ setCopied(true);
+ toast.success("API key copied to clipboard!");
+ setTimeout(() => setCopied(false), 2000);
+ } catch {
+ toast.error("Failed to copy API key");
+ }
+ };
+
+ const handleOpenShortcut = () => {
+ if (!selectedShortcutType) {
+ toast.error("No shortcut type selected");
+ return;
+ }
+
+ if (selectedShortcutType === "add") {
+ window.open(ADD_MEMORY_SHORTCUT_URL, "_blank");
+ } else if (selectedShortcutType === "search") {
+ window.open(SEARCH_MEMORY_SHORTCUT_URL, "_blank");
+ }
+ };
+
+ const handleDialogClose = (open: boolean) => {
+ setShowApiKeyModal(open);
+ if (!open) {
+ // Reset state when dialog closes
+ setSelectedShortcutType(null);
+ setApiKey("");
+ setCopied(false);
+ }
+ };
+
+ return (
+
+ {/* iOS Shortcuts */}
+
+
+
+
+
+
+
+
+ Apple shortcuts
+
+
+ Add memories directly from iPhone, iPad or Mac.
+
+
+
+
+
+
+
+
+
+
+ {/* Chrome Extension */}
+
+
+
+
+
+
+
+
+
+ Chrome Extension
+
+
+ Coming Soon
+
+
+
+ Save web content with one click
+
+
+
+
+
+
+ {/* Connections Section */}
+
+
+
+
+
+
+
+
+ Connections
+
+
+ Connect your accounts to sync document.
+
+ {!isProUser && (
+
+ Connections require a Pro subscription
+
+ )}
+
+
+
+ {/* Show upgrade prompt for free users */}
+ {!isProUser && (
+
+
+ 🔌 Connections are a Pro feature
+
+
+ Connect Google Drive, Notion, OneDrive and more to automatically
+ sync your documents.
+
+
+
+ )}
+
+ {/* All Connections with Status */}
+ {connectionsLoading ? (
+
+ {Object.keys(CONNECTORS).map((_, i) => (
+
+
+
+ ))}
+
+ ) : (
+
+ {Object.entries(CONNECTORS).map(([provider, config], index) => {
+ const Icon = config.icon;
+ const connection = connections.find(
+ (conn) => conn.provider === provider,
+ );
+ const isConnected = !!connection;
+
+ return (
+
+
+
+
+
+
+
+
+ {config.title}
+
+ {isConnected ? (
+
+ ) : (
+
+ )}
+
+
+ {config.description}
+
+ {connection?.email && (
+
+ {connection.email}
+
+ )}
+
+
+
+
+ {isConnected ? (
+
+
+
+ ) : (
+
+
+
+
+
+
+ )}
+
+
+ );
+ })}
+
+ )}
+
+
+
+
+
+ More integrations are coming soon! Have a suggestion? Share it with us
+ on{" "}
+
+ X
+
+ .
+
+
+
+ {/* API Key Modal */}
+
+
+ );
+}
diff --git a/apps/web/public/images/ios-shortcuts.png b/apps/web/public/images/ios-shortcuts.png
new file mode 100644
index 00000000..044fb683
Binary files /dev/null and b/apps/web/public/images/ios-shortcuts.png differ
diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts
index 1bfd1ea8..fde5bce1 100644
--- a/packages/lib/constants.ts
+++ b/packages/lib/constants.ts
@@ -1,4 +1,13 @@
const BIG_DIMENSIONS_NEW = 1536
const DEFAULT_PROJECT_ID = "sm_project_default"
+const SEARCH_MEMORY_SHORTCUT_URL =
+ "https://www.icloud.com/shortcuts/f2b5c544372844a38ab4c6900e2a88de"
+const ADD_MEMORY_SHORTCUT_URL =
+ "https://www.icloud.com/shortcuts/ec33b029b2c7481d89eda7640dbb7688"
-export { BIG_DIMENSIONS_NEW, DEFAULT_PROJECT_ID }
+export {
+ BIG_DIMENSIONS_NEW,
+ DEFAULT_PROJECT_ID,
+ SEARCH_MEMORY_SHORTCUT_URL,
+ ADD_MEMORY_SHORTCUT_URL,
+}