Fix Raycast API key bug and redesign setup modal (#960)

- Fix `authClient.apiKey.create()` returning undefined: better-auth resolves to `{ data, error }`, not the key directly. Use `res.data.key` (with `res.error` handling) in both `RaycastDetail` and `settings/integrations.tsx`.
- Extract `RaycastSetupModal` and reuse it across the nova home Raycast card and the Settings → Integrations Raycast modal. The new modal matches the plugins-connect design system (icon box, pill close, inset card with shared `InstallSteps`, install pill).
- Add `/settings/integrations` redirect page so `app.supermemory.ai/settings/integrations?q=raycast` lands on the Integrations tab and auto-opens the Raycast modal (the `?q=raycast` auto-trigger logic already existed; the route was the missing piece).
This commit is contained in:
MaheshtheDev 2026-05-17 23:15:04 +00:00
parent 1706752668
commit e1e59b9fbd
4 changed files with 172 additions and 270 deletions

View file

@ -0,0 +1,16 @@
"use client"
import { useEffect } from "react"
import { useRouter, useSearchParams } from "next/navigation"
export default function SettingsIntegrationsPage() {
const router = useRouter()
const searchParams = useSearchParams()
useEffect(() => {
const qs = searchParams.toString()
router.replace(`/settings${qs ? `?${qs}` : ""}#integrations`)
}, [router, searchParams])
return null
}

View file

@ -7,17 +7,11 @@ import { authClient } from "@lib/auth"
import { useAuth } from "@lib/auth-context"
import { generateId } from "@lib/generate-id"
import { RAYCAST_EXTENSION_URL } from "@lib/constants"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogPortal,
} from "@ui/components/dialog"
import { useMutation } from "@tanstack/react-query"
import { Check, Copy, Download, Key, Loader } from "lucide-react"
import { useId, useState } from "react"
import { Download, Key, Loader } from "lucide-react"
import { useState } from "react"
import { toast } from "sonner"
import { RaycastSetupModal } from "./raycast-setup-modal"
function PillButton({
children,
@ -52,19 +46,6 @@ export function RaycastDetail() {
const { org } = useAuth()
const [showModal, setShowModal] = useState(false)
const [apiKey, setApiKey] = useState("")
const [copied, setCopied] = useState(false)
const apiKeyId = useId()
const handleCopy = async (key: string) => {
try {
await navigator.clipboard.writeText(key)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
toast.success("API key copied to clipboard!")
} catch {
toast.error("Failed to copy API key")
}
}
const createKeyMutation = useMutation({
mutationFn: async () => {
@ -74,13 +55,14 @@ export function RaycastDetail() {
name: `raycast-${generateId().slice(0, 8)}`,
prefix: `sm_${org.id}_`,
})
return res.key
if (res.error)
throw new Error(res.error.message ?? "Failed to create API key")
if (!res.data?.key) throw new Error("API key missing from response")
return res.data.key
},
onSuccess: (key) => {
setApiKey(key)
setShowModal(true)
setCopied(false)
handleCopy(key)
},
onError: (error) => {
toast.error("Failed to create API key", {
@ -146,111 +128,14 @@ export function RaycastDetail() {
</div>
</div>
<Dialog
<RaycastSetupModal
open={showModal}
onOpenChange={(open: boolean) => {
onOpenChange={(open) => {
setShowModal(open)
if (!open) {
setApiKey("")
setCopied(false)
}
if (!open) setApiKey("")
}}
>
<DialogPortal>
<DialogContent className="bg-[#14161A] border border-white/10 text-[#FAFAFA] md:max-w-md z-100">
<DialogHeader>
<DialogTitle
className={cn(
dmSans125ClassName(),
"text-[#FAFAFA] text-lg font-semibold",
)}
>
Setup Raycast Extension
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<label
htmlFor={apiKeyId}
className={cn(
dmSans125ClassName(),
"text-sm font-medium text-[#737373]",
)}
>
Your Raycast API Key
</label>
<div className="flex items-center gap-2">
<input
id={apiKeyId}
type="text"
value={apiKey}
readOnly
className={cn(
"flex-1 bg-[#0D121A] border border-white/10 rounded-lg px-3 py-2 text-sm text-[#FAFAFA] font-mono",
dmSans125ClassName(),
)}
/>
<button
type="button"
onClick={() => handleCopy(apiKey)}
className="p-2 rounded-lg bg-[#0D121A] border border-white/10 text-[#737373] hover:text-[#FAFAFA] transition-colors"
>
{copied ? (
<Check className="size-4 text-[#4BA0FA]" />
) : (
<Copy className="size-4" />
)}
</button>
</div>
</div>
<div className="space-y-3">
<h4
className={cn(
dmSans125ClassName(),
"text-sm font-medium text-[#737373]",
)}
>
Follow these steps:
</h4>
<div className="space-y-2">
{[
"Install the Raycast extension from the Raycast Store",
"Open Raycast preferences and paste your API key",
'Use "Add Memory" or "Search Memories" commands!',
].map((text, i) => (
<div key={text} className="flex items-start gap-3">
<div className="shrink-0 size-6 bg-[#FF6363]/20 text-[#FF6363] rounded-full flex items-center justify-center text-xs font-medium">
{i + 1}
</div>
<p
className={cn(
dmSans125ClassName(),
"text-sm text-[#737373]",
)}
>
{text}
</p>
</div>
))}
</div>
</div>
<button
type="button"
onClick={() => window.open(RAYCAST_EXTENSION_URL, "_blank")}
className={cn(
"w-full flex items-center justify-center gap-2",
"bg-[#FF6363] hover:bg-[#FF6363]/90 text-white",
"rounded-lg h-11 px-4 font-medium text-sm transition-colors",
dmSans125ClassName(),
)}
>
<RaycastIcon className="size-4" />
Install Extension
</button>
</div>
</DialogContent>
</DialogPortal>
</Dialog>
apiKey={apiKey}
/>
</>
)
}

View file

@ -0,0 +1,132 @@
"use client"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { Download, X } from "lucide-react"
import { cn } from "@lib/utils"
import { dmSans125ClassName } from "@/lib/fonts"
import { RAYCAST_EXTENSION_URL } from "@lib/constants"
import { RaycastIcon } from "@/components/integration-icons"
import { Dialog, DialogContent, DialogTitle } from "@ui/components/dialog"
import type { InstallStep } from "@/lib/plugin-catalog"
import { INSET, InstallSteps } from "./install-steps"
const RAYCAST_STEPS: InstallStep[] = [
{
title: "Copy your API key",
description: "You won't be able to see it again — store it somewhere safe.",
code: "sm_...",
copyLabel: "API key",
secret: true,
},
{
title: "Install the Raycast extension",
description: "Open the Supermemory extension page in the Raycast Store.",
},
{
title: "Paste your key in Raycast preferences",
description:
"Open Raycast preferences → Extensions → Supermemory, then paste the key above.",
},
{
title: 'Run "Add Memory" or "Search Memories"',
description: "Trigger Raycast and start using Supermemory from anywhere.",
},
]
function RaycastIconBox() {
return (
<div
className={cn(
"flex size-10 shrink-0 items-center justify-center rounded-[10px] bg-[#080B0F]",
"shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.6)]",
)}
>
<RaycastIcon className="size-6" />
</div>
)
}
export function RaycastSetupModal({
open,
onOpenChange,
apiKey,
}: {
open: boolean
onOpenChange: (open: boolean) => void
apiKey: string
}) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent
showCloseButton={false}
style={{
boxShadow:
"0 2.842px 14.211px 0 rgba(0,0,0,0.25), 0.711px 0.711px 0.711px 0 rgba(255,255,255,0.10) inset",
}}
className={cn(
dmSans125ClassName(),
"flex max-h-[88dvh] flex-col gap-3 overflow-hidden border border-white/[0.12] bg-[#1B1F24] p-0 px-3 pt-3 pb-4 rounded-2xl md:px-4 sm:max-w-[560px] sm:rounded-[22px]",
)}
>
<DialogTitle className="sr-only">Set up Raycast Extension</DialogTitle>
<div className="flex shrink-0 items-center gap-3">
<RaycastIconBox />
<div className="min-w-0 flex-1">
<p
className={cn(
dmSans125ClassName(),
"truncate text-[16px] font-semibold leading-tight text-[#FAFAFA]",
)}
>
Set up Raycast Extension
</p>
<p
className={cn(
dmSans125ClassName(),
"mt-0.5 truncate text-[12px] text-[#A1A1AA]",
)}
>
Copy your key and follow these steps to finish.
</p>
</div>
<DialogPrimitive.Close
type="button"
aria-label="Close"
className={cn(
"flex size-7 items-center justify-center rounded-full bg-[#0D121A] transition-opacity hover:opacity-80 focus:outline-none",
INSET,
)}
>
<X className="size-4 text-[#737373]" />
</DialogPrimitive.Close>
</div>
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto">
<div
className={cn(
"min-w-0 rounded-[14px] bg-[#14161A] p-4 sm:p-5",
INSET,
)}
>
<InstallSteps steps={RAYCAST_STEPS} apiKey={apiKey} />
</div>
</div>
<div className="flex shrink-0 items-center justify-end gap-2">
<button
type="button"
onClick={() => window.open(RAYCAST_EXTENSION_URL, "_blank")}
className={cn(
dmSans125ClassName(),
"flex h-9 items-center gap-1.5 rounded-full bg-[#0D121A] px-5 text-[13px] font-medium text-[#FAFAFA] transition-opacity hover:opacity-80",
INSET,
)}
>
<Download className="size-3.5 text-[#A1A1AA]" /> Install extension
</button>
</div>
</DialogContent>
</Dialog>
)
}

View file

@ -30,6 +30,7 @@ import {
AppleShortcutsIcon,
RaycastIcon,
} from "@/components/integration-icons"
import { RaycastSetupModal } from "@/components/integrations/raycast-setup-modal"
function SectionTitle({ children }: { children: React.ReactNode }) {
return (
@ -128,20 +129,13 @@ export default function Integrations() {
// Raycast state
const [showRaycastApiKeyModal, setShowRaycastApiKeyModal] = useState(false)
const [raycastApiKey, setRaycastApiKey] = useState<string>("")
const [raycastCopied, setRaycastCopied] = useState(false)
const [hasTriggeredRaycast, setHasTriggeredRaycast] = useState(false)
const raycastApiKeyId = useId()
const handleCopyApiKey = async (key: string, isRaycast = false) => {
const handleCopyApiKey = async (key: string) => {
try {
await navigator.clipboard.writeText(key)
if (isRaycast) {
setRaycastCopied(true)
setTimeout(() => setRaycastCopied(false), 2000)
} else {
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
setCopied(true)
setTimeout(() => setCopied(false), 2000)
toast.success("API key copied to clipboard!")
} catch {
toast.error("Failed to copy API key")
@ -187,13 +181,14 @@ export default function Integrations() {
name: `raycast-${generateId().slice(0, 8)}`,
prefix: `sm_${org.id}_`,
})
return res.key
if (res.error)
throw new Error(res.error.message ?? "Failed to create API key")
if (!res.data?.key) throw new Error("API key missing from response")
return res.data.key
},
onSuccess: (key) => {
setRaycastApiKey(key)
setShowRaycastApiKeyModal(true)
setRaycastCopied(false)
handleCopyApiKey(key, true)
},
onError: (error) => {
toast.error("Failed to create Raycast API key", {
@ -260,10 +255,7 @@ export default function Integrations() {
const handleRaycastDialogClose = (open: boolean) => {
setShowRaycastApiKeyModal(open)
if (!open) {
setRaycastApiKey("")
setRaycastCopied(false)
}
if (!open) setRaycastApiKey("")
}
return (
@ -564,134 +556,11 @@ export default function Integrations() {
</DialogPortal>
</Dialog>
<Dialog
<RaycastSetupModal
open={showRaycastApiKeyModal}
onOpenChange={handleRaycastDialogClose}
>
<DialogPortal>
<DialogContent
id="raycast-api-key-modal"
className="bg-[#14161A] border border-white/10 text-[#FAFAFA] md:max-w-md z-100"
>
<DialogHeader>
<DialogTitle
className={cn(
dmSans125ClassName(),
"text-[#FAFAFA] text-lg font-semibold",
)}
>
Setup Raycast Extension
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div id="raycast-api-key-section" className="space-y-2">
<label
htmlFor={raycastApiKeyId}
className={cn(
dmSans125ClassName(),
"text-sm font-medium text-[#737373]",
)}
>
Your Raycast API Key
</label>
<div className="flex items-center gap-2">
<input
id={raycastApiKeyId}
type="text"
value={raycastApiKey}
readOnly
className={cn(
"flex-1 bg-[#0D121A] border border-white/10 rounded-lg px-3 py-2 text-sm text-[#FAFAFA] font-mono",
dmSans125ClassName(),
)}
/>
<button
type="button"
onClick={() => handleCopyApiKey(raycastApiKey, true)}
className="p-2 rounded-lg bg-[#0D121A] border border-white/10 text-[#737373] hover:text-[#FAFAFA] transition-colors"
>
{raycastCopied ? (
<Check className="size-4 text-[#4BA0FA]" />
) : (
<Copy className="size-4" />
)}
</button>
</div>
</div>
<div id="raycast-steps" className="space-y-3">
<h4
className={cn(
dmSans125ClassName(),
"text-sm font-medium text-[#737373]",
)}
>
Follow these steps:
</h4>
<div className="space-y-2">
<div className="flex items-start gap-3">
<div className="shrink-0 size-6 bg-[#FF6363]/20 text-[#FF6363] rounded-full flex items-center justify-center text-xs font-medium">
1
</div>
<p
className={cn(
dmSans125ClassName(),
"text-sm text-[#737373]",
)}
>
Install the Raycast extension from the Raycast Store
</p>
</div>
<div className="flex items-start gap-3">
<div className="shrink-0 size-6 bg-[#FF6363]/20 text-[#FF6363] rounded-full flex items-center justify-center text-xs font-medium">
2
</div>
<p
className={cn(
dmSans125ClassName(),
"text-sm text-[#737373]",
)}
>
Open Raycast preferences and paste your API key
</p>
</div>
<div className="flex items-start gap-3">
<div className="shrink-0 size-6 bg-[#FF6363]/20 text-[#FF6363] rounded-full flex items-center justify-center text-xs font-medium">
3
</div>
<p
className={cn(
dmSans125ClassName(),
"text-sm text-[#737373]",
)}
>
Use "Add Memory" or "Search Memories" commands!
</p>
</div>
</div>
</div>
<div className="flex gap-2 pt-2">
<button
type="button"
onClick={handleRaycastInstall}
className={cn(
"flex-1 flex items-center justify-center gap-2",
"bg-[#FF6363] hover:bg-[#FF6363]/90 text-white",
"rounded-lg h-11 px-4 font-medium text-sm",
"transition-colors",
dmSans125ClassName(),
)}
>
<RaycastIcon className="size-4" />
Install Extension
</button>
</div>
</div>
</DialogContent>
</DialogPortal>
</Dialog>
apiKey={raycastApiKey}
/>
</div>
)
}