diff --git a/apps/web/components/integrations-view.tsx b/apps/web/components/integrations-view.tsx
index 57c6aeb76..dc0876dab 100644
--- a/apps/web/components/integrations-view.tsx
+++ b/apps/web/components/integrations-view.tsx
@@ -73,6 +73,10 @@ import {
type InstallStep,
} from "@/lib/plugin-catalog"
import { INSET, InstallSteps, PillButton } from "./integrations/install-steps"
+import {
+ ShortcutsConnectButtons,
+ useShortcutsConnect,
+} from "./integrations/shortcuts-detail"
import { MCPSteps } from "./mcp-modal/mcp-detail-view"
import { GranolaConnectModal } from "./granola-connect-modal"
import { detectPluginSpace, detectPluginSource } from "@/lib/plugin-space"
@@ -804,6 +808,7 @@ function ItemInfoButton({
function ItemInfoDialog({
actionSlot,
docsUrl,
+ hideDismiss,
icon,
id,
kind,
@@ -813,6 +818,7 @@ function ItemInfoDialog({
}: {
actionSlot: ReactNode
docsUrl?: string
+ hideDismiss?: boolean
icon: ReactNode
id: string
kind: ItemKind
@@ -942,17 +948,19 @@ function ItemInfoDialog({
-
-
closeWithReason("im_good")}
- className={cn(
- "px-3 py-2 text-[13px] font-medium text-[#737373] transition-colors hover:text-[#fafafa]",
- dmSansClassName(),
- )}
- >
- I'm good
-
+
+ {!hideDismiss && (
+
closeWithReason("im_good")}
+ className={cn(
+ "px-3 py-2 text-[13px] font-medium text-[#737373] transition-colors hover:text-[#fafafa]",
+ dmSansClassName(),
+ )}
+ >
+ I'm good
+
+ )}
{/* biome-ignore lint/a11y/noStaticElementInteractions: closes the info dialog after the nested action button runs. */}
{/* biome-ignore lint/a11y/useKeyWithClickEvents: keyboard handling stays on the nested real button. */}
closeWithReason("action")}>{actionSlot}
@@ -1766,6 +1774,7 @@ function ItemCard({
)
}
+ if (item.id === "shortcuts") {
+ return
+ }
return (
{
@@ -3062,6 +3075,12 @@ export function IntegrationsView({
}
return renderRight(item)
}
+ case "client": {
+ if (item.id === "shortcuts") {
+ return
+ }
+ return renderRight(item)
+ }
default:
return renderRight(item)
}
@@ -3155,6 +3174,7 @@ export function IntegrationsView({
return (
+ {shortcutsConnect.dialog}
void
+ onClick?: (e: React.MouseEvent) => void
disabled?: boolean
+ className?: string
}) {
return (
{children}
@@ -52,17 +57,14 @@ function PillButton({
)
}
-export function ShortcutsDetail() {
+export function useShortcutsConnect() {
const { org } = useAuth()
- const [showApiKeyModal, setShowApiKeyModal] = useState(false)
+ const [showSetupDialog, setShowSetupDialog] = useState(false)
const [apiKey, setApiKey] = useState("")
const [copied, setCopied] = useState(false)
- const [selectedShortcutType, setSelectedShortcutType] = useState<
- "add" | "search" | null
- >(null)
- const apiKeyId = useId()
+ const [shortcutType, setShortcutType] = useState(null)
- const handleCopyApiKey = async (key: string) => {
+ const copyApiKey = async (key: string) => {
try {
await navigator.clipboard.writeText(key)
setCopied(true)
@@ -73,20 +75,24 @@ export function ShortcutsDetail() {
}
}
- const createApiKeyMutation = useMutation({
+ const createKeyMutation = useMutation({
mutationFn: async () => {
+ if (!org?.id) throw new Error("Organization ID is required")
const res = await authClient.apiKey.create({
- metadata: { organizationId: org?.id, type: "ios-shortcut" },
+ metadata: { organizationId: org.id, type: "ios-shortcut" },
name: `ios-${generateId().slice(0, 8)}`,
- prefix: `sm_${org?.id}_`,
+ 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)
- setShowApiKeyModal(true)
+ setShowSetupDialog(true)
setCopied(false)
- handleCopyApiKey(key)
+ copyApiKey(key)
},
onError: (error) => {
toast.error("Failed to create API key", {
@@ -95,19 +101,224 @@ export function ShortcutsDetail() {
},
})
- const handleShortcutClick = (type: "add" | "search") => {
- setSelectedShortcutType(type)
- createApiKeyMutation.mutate()
+ const connect = (type: ShortcutType) => {
+ if (createKeyMutation.isPending) return
+ setShortcutType(type)
+ createKeyMutation.mutate()
}
- const handleOpenShortcut = () => {
- if (selectedShortcutType === "add") {
+ const openShortcut = () => {
+ if (shortcutType === "add") {
window.open(ADD_MEMORY_SHORTCUT_URL, "_blank")
- } else if (selectedShortcutType === "search") {
+ } else if (shortcutType === "search") {
window.open(SEARCH_MEMORY_SHORTCUT_URL, "_blank")
}
}
+ const dialog = (
+ copyApiKey(apiKey)}
+ onOpenChange={(open) => {
+ setShowSetupDialog(open)
+ if (!open) {
+ setShortcutType(null)
+ setApiKey("")
+ setCopied(false)
+ }
+ }}
+ onOpenShortcut={openShortcut}
+ open={showSetupDialog}
+ shortcutType={shortcutType}
+ />
+ )
+
+ return {
+ connect,
+ dialog,
+ isPending: createKeyMutation.isPending,
+ pendingType: createKeyMutation.isPending ? shortcutType : null,
+ }
+}
+
+export type ShortcutsConnectController = ReturnType
+
+export function ShortcutsConnectButtons({
+ controller,
+}: {
+ controller: ShortcutsConnectController
+}) {
+ const { connect, isPending, pendingType } = controller
+ return (
+
+
{
+ e.stopPropagation()
+ connect("add")
+ }}
+ disabled={isPending}
+ >
+ {pendingType === "add" ? (
+
+ ) : (
+
+ )}
+
+ {pendingType === "add" ? "Creating..." : "Add memory shortcut"}
+
+
+
{
+ e.stopPropagation()
+ connect("search")
+ }}
+ disabled={isPending}
+ >
+ {pendingType === "search" ? (
+
+ ) : (
+
+ )}
+
+ {pendingType === "search" ? "Creating..." : "Search memory shortcut"}
+
+
+
+ )
+}
+
+function ShortcutsSetupDialog({
+ apiKey,
+ copied,
+ onCopy,
+ onOpenChange,
+ onOpenShortcut,
+ open,
+ shortcutType,
+}: {
+ apiKey: string
+ copied: boolean
+ onCopy: () => void
+ onOpenChange: (open: boolean) => void
+ onOpenShortcut: () => void
+ open: boolean
+ shortcutType: ShortcutType | null
+}) {
+ const apiKeyId = useId()
+ return (
+
+
+
+
+
+ Setup Apple Shortcut
+
+
+
+
+
+ Your API Key
+
+
+
+
+ {copied ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ Follow these steps:
+
+
+ {[
+ 'Click "Add to Shortcuts" below to open the shortcut',
+ "Paste your API key when prompted",
+ "Start using your shortcut!",
+ ].map((text, i) => (
+
+
+ {i + 1}
+
+
+ {text}
+
+
+ ))}
+
+
+
+
+ Add to Shortcuts
+
+
+
+
+
+ )
+}
+
+export function ShortcutsDetail() {
+ const controller = useShortcutsConnect()
+ const { connect, dialog, isPending, pendingType } = controller
+
return (
<>
-
handleShortcutClick("add")}
- disabled={createApiKeyMutation.isPending}
- >
- {createApiKeyMutation.isPending &&
- selectedShortcutType === "add" ? (
+ connect("add")} disabled={isPending}>
+ {pendingType === "add" ? (
) : (
)}
- {createApiKeyMutation.isPending &&
- selectedShortcutType === "add"
- ? "Creating..."
- : "Add memory shortcut"}
+ {pendingType === "add" ? "Creating..." : "Add memory shortcut"}
- handleShortcutClick("search")}
- disabled={createApiKeyMutation.isPending}
- >
- {createApiKeyMutation.isPending &&
- selectedShortcutType === "search" ? (
+ connect("search")} disabled={isPending}>
+ {pendingType === "search" ? (
) : (
)}
- {createApiKeyMutation.isPending &&
- selectedShortcutType === "search"
+ {pendingType === "search"
? "Creating..."
: "Search memory shortcut"}
@@ -177,120 +376,7 @@ export function ShortcutsDetail() {
-
-
{
- setShowApiKeyModal(open)
- if (!open) {
- setSelectedShortcutType(null)
- setApiKey("")
- setCopied(false)
- }
- }}
- >
-
-
-
-
- Setup Apple Shortcut
-
-
-
-
-
- Your API Key
-
-
-
- handleCopyApiKey(apiKey)}
- className="p-2 rounded-lg bg-[#0D121A] border border-white/10 text-[#737373] hover:text-[#FAFAFA] transition-colors"
- >
- {copied ? (
-
- ) : (
-
- )}
-
-
-
-
-
- Follow these steps:
-
-
- {[
- 'Click "Add to Shortcuts" below to open the shortcut',
- "Paste your API key when prompted",
- "Start using your shortcut!",
- ].map((text, i) => (
-
-
- {i + 1}
-
-
- {text}
-
-
- ))}
-
-
-
-
- Add to Shortcuts
-
-
-
-
-
+ {dialog}
>
)
}