mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
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:
parent
1706752668
commit
e1e59b9fbd
4 changed files with 172 additions and 270 deletions
16
apps/web/app/(app)/settings/integrations/page.tsx
Normal file
16
apps/web/app/(app)/settings/integrations/page.tsx
Normal 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
|
||||
}
|
||||
|
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
132
apps/web/components/integrations/raycast-setup-modal.tsx
Normal file
132
apps/web/components/integrations/raycast-setup-modal.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue