Make integrations page public for guests (#1073)

## Summary
- Allow logged-out users to load `/?view=integrations` without being redirected to login
- Add a public integrations header with Supermemory branding and a login CTA
- Run IntegrationsView in guest mode with static catalog data only, no auth-backed queries
- Route all guest Connect actions to login with a redirect back to the integrations page

<img width="1680" height="1009" alt="Screenshot 2026-06-09 at 9 32 33 PM" src="https://github.com/user-attachments/assets/111f2b1a-5224-47ae-8152-e1349c05842d" />
This commit is contained in:
ved015 2026-06-09 17:32:00 +00:00
parent 170133cf83
commit c71acbe8ae
5 changed files with 113 additions and 20 deletions

View file

@ -582,6 +582,7 @@ export default function NewPage() {
viewMode === "dashboard" || (viewMode === "graph" && isMobile)
const isGraphMode = viewMode === "graph"
const showBottomNav = isMobile && !!session && !isChatView
const isPublicIntegrations = !session && viewMode === "integrations"
return (
<HotkeysProvider>
@ -608,7 +609,9 @@ export default function NewPage() {
/>
</div>
)}
{!session && viewMode === "mcp" ? (
{isPublicIntegrations ? (
<PublicHeader variant="integrations" />
) : !session && viewMode === "mcp" ? (
<PublicHeader />
) : (
<Header
@ -660,7 +663,7 @@ export default function NewPage() {
</div>
) : viewMode === "integrations" ? (
<div className="min-h-0 min-w-0 flex-1 p-4 pt-2! md:p-6 md:pr-0">
<IntegrationsView />
<IntegrationsView publicMode={isPublicIntegrations} />
</div>
) : viewMode === "mcp" ? (
<MCPDetailView

View file

@ -22,11 +22,14 @@ export function EnsureWorkspace({ children }: { children: React.ReactNode }) {
const searchParams = useSearchParams()
const { session, organizations, isRestoring } = useAuth()
const isMcpPublicPage = searchParams.get("view") === "mcp"
const isPublicAppPage =
pathname === "/" &&
["integrations", "mcp"].includes(searchParams.get("view") ?? "")
const isGuestPublicAppPage = isPublicAppPage && !session
const isOnboarding = pathname.startsWith("/onboarding")
useEffect(() => {
if (isMcpPublicPage) return
if (isGuestPublicAppPage) return
if (isRestoring) return
if (!session) {
router.replace(
@ -44,11 +47,11 @@ export function EnsureWorkspace({ children }: { children: React.ReactNode }) {
isRestoring,
isOnboarding,
router,
isMcpPublicPage,
isGuestPublicAppPage,
])
const showLoading =
!isMcpPublicPage &&
!isGuestPublicAppPage &&
(isRestoring ||
(!session && !isRestoring) ||
(session && organizations === null) ||

View file

@ -504,7 +504,41 @@ export function Header({ onAddMemory, onOpenSearch }: HeaderProps) {
)
}
export function PublicHeader() {
export function PublicHeader({
variant = "default",
}: {
variant?: "default" | "integrations"
}) {
if (variant === "integrations") {
return (
<div className="relative z-10 flex shrink-0 items-center justify-between gap-2 p-2.5 md:p-3">
<Link
href="/?view=integrations"
className="flex items-center gap-2 transition-opacity hover:opacity-90"
>
<Logo className="h-6 md:h-7" />
<p className="text-base leading-none font-medium text-white/90 sm:text-lg">
supermemory
</p>
</Link>
<Link href="/login?redirect=%2F%3Fview%3Dintegrations">
<button
type="button"
className={cn(
"flex h-10 cursor-pointer items-center gap-2 rounded-full px-4 text-[14px] font-medium text-white transition-opacity hover:opacity-95 sm:px-5 sm:text-[15px]",
"bg-[linear-gradient(100deg,#426BFF_0%,#2D1CFF_100%)] shadow-[inset_0_1px_0_rgba(255,255,255,0.16)]",
dmSansClassName(),
)}
>
<Logo className="h-4 w-5 shrink-0" />
Log in with Supermemory
</button>
</Link>
</div>
)
}
return (
<div className="relative z-10 flex shrink-0 items-center justify-between gap-2 p-2.5 md:p-3">
<Link

View file

@ -1460,13 +1460,18 @@ function SectionRail({
)
}
export function IntegrationsView() {
export function IntegrationsView({
publicMode = false,
}: {
publicMode?: boolean
}) {
const { setViewMode } = useViewMode()
const queryClient = useQueryClient()
const { org } = useAuth()
const autumn = useCustomer()
const hasProProduct = hasActivePlan(autumn.data?.subscriptions, "api_pro")
const isAutumnLoading = autumn.isLoading
const autumn = useCustomer({ queryOptions: { enabled: !publicMode } })
const hasProProduct =
!publicMode && hasActivePlan(autumn.data?.subscriptions, "api_pro")
const isAutumnLoading = !publicMode && autumn.isLoading
const [connectingPlugin, setConnectingPlugin] = useState<string | null>(null)
const [connectingProvider, setConnectingProvider] =
@ -1493,6 +1498,7 @@ export function IntegrationsView() {
if (!res.ok) throw new Error("Failed to fetch plugins")
return (await res.json()) as { plugins: string[] }
},
enabled: !publicMode,
queryKey: ["plugins"],
})
@ -1507,7 +1513,7 @@ export function IntegrationsView() {
return response.data as Connection[]
},
staleTime: 30 * 1000,
enabled: hasProProduct,
enabled: !publicMode && hasProProduct,
})
const { data: apiKeys = [], refetch: refetchKeys } = useQuery<ListedApiKey[]>(
@ -1524,7 +1530,7 @@ export function IntegrationsView() {
const data = (await res.json()) as { keys?: ListedApiKey[] }
return data.keys ?? []
},
enabled: !!org?.id,
enabled: !publicMode && !!org?.id,
staleTime: 30 * 1000,
},
)
@ -1678,7 +1684,15 @@ export function IntegrationsView() {
}
}
const availablePluginIds = pluginsData?.plugins ?? Object.keys(PLUGIN_CATALOG)
const redirectToLogin = useCallback(() => {
const loginUrl = new URL("/login", window.location.origin)
loginUrl.searchParams.set("redirect", window.location.href)
window.location.assign(loginUrl.toString())
}, [])
const availablePluginIds = publicMode
? Object.keys(PLUGIN_CATALOG)
: (pluginsData?.plugins ?? Object.keys(PLUGIN_CATALOG))
const enabledPluginIds = new Set(
availablePluginIds.filter((id) => PLUGIN_CATALOG[id]),
)
@ -1714,6 +1728,7 @@ export function IntegrationsView() {
const isItemConnected = useCallback(
(item: Item): boolean => {
if (publicMode) return false
if (item.kind === "plugin") {
return activePluginById.has(item.pluginId)
}
@ -1722,7 +1737,7 @@ export function IntegrationsView() {
}
return false
},
[activePluginById, connectionsByProvider],
[activePluginById, connectionsByProvider, publicMode],
)
const counts = useMemo<Record<CategoryFilter, number>>(
@ -1780,6 +1795,10 @@ export function IntegrationsView() {
),
ctaLabel: "Connect",
onCta: () => {
if (publicMode) {
redirectToLogin()
return
}
window.open(POKE_RECIPE_URL, "_blank", "noopener,noreferrer")
},
},
@ -1802,6 +1821,10 @@ export function IntegrationsView() {
docsUrl: "https://supermemory.ai/docs/supermemory-mcp/introduction",
ctaLabel: "Connect",
onCta: () => {
if (publicMode) {
redirectToLogin()
return
}
void setMcpClient(null)
setViewMode("mcp")
},
@ -1831,12 +1854,18 @@ export function IntegrationsView() {
/>
),
docsUrl: "https://supermemory.ai/docs/integrations/claude-code",
ctaLabel: claudeCodeConnected
? "Active"
: claudeCodeNeedsPro
? "Upgrade"
: "Connect",
ctaLabel: publicMode
? "Connect"
: claudeCodeConnected
? "Active"
: claudeCodeNeedsPro
? "Upgrade"
: "Connect",
onCta: () => {
if (publicMode) {
redirectToLogin()
return
}
if (claudeCodeConnected) return
if (claudeCodeNeedsPro) {
handleUpgrade()
@ -1855,6 +1884,10 @@ export function IntegrationsView() {
backdrop: <ChromeIcon className="size-96" />,
ctaLabel: "Connect",
onCta: () => {
if (publicMode) {
redirectToLogin()
return
}
window.open(CHROME_EXTENSION_URL, "_blank", "noopener,noreferrer")
analytics.onboardingChromeExtensionClicked({ source: "integrations" })
},
@ -1885,6 +1918,19 @@ export function IntegrationsView() {
})
const renderRight = (item: Item): ReactNode => {
if (publicMode) {
return (
<PillButton
onClick={() => {
trackCard(item)
redirectToLogin()
}}
>
Connect
</PillButton>
)
}
switch (item.kind) {
case "plugin": {
const activeKey = activePluginById.get(item.pluginId)
@ -2065,6 +2111,8 @@ export function IntegrationsView() {
}
const renderStatus = (item: Item): ReactNode => {
if (publicMode) return null
switch (item.kind) {
case "plugin": {
const activeKey = activePluginById.get(item.pluginId)

View file

@ -31,6 +31,11 @@ export default async function proxy(request: Request) {
return NextResponse.next()
}
// Integrations index is public in guest mode; actions still require login.
if (url.pathname === "/" && url.searchParams.get("view") === "integrations") {
return NextResponse.next()
}
if (url.pathname.startsWith("/api/")) {
if (!sessionCookie) {
console.debug("[MIDDLEWARE] API route without session, returning 401")