mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
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:
parent
170133cf83
commit
c71acbe8ae
5 changed files with 113 additions and 20 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) ||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue