| @@ -1,6 +1,7 @@ |
| 1 | 1 | "use client"; |
| 2 | 2 | |
| 3 | | −import { useEffect } from "react"; |
| 3 | +import { useEffect, useRef } from "react"; |
| 4 | +import { useQuery as useTanstackQuery } from "@tanstack/react-query"; |
| 4 | 5 | |
| 5 | 6 | type PopupMessage = |
| 6 | 7 | | { |
| @@ -28,45 +29,47 @@ function fallbackErrorMessage(error: unknown): PopupMessage { · 21 unmodified lines |
| 28 | 29 | }; |
| 29 | 30 | } |
| 30 | 31 | |
| 32 | +async function fetchPopupResult(): Promise<PopupMessage> { |
| 33 | + const response = await fetch("/mcp/oauth/result", { |
| 34 | + method: "GET", |
| 35 | + cache: "no-store", |
| 36 | + credentials: "same-origin", |
| 37 | + }); |
| 38 | + |
| 39 | + const data = await response.json() as PopupMessage; |
| 40 | + if (!data || data.type !== "executor:mcp-oauth-result") { |
| 41 | + throw new Error("Invalid OAuth completion payload"); |
| 42 | + } |
| 43 | + return data; |
| 44 | +} |
| 45 | + |
| 31 | 46 | export default function McpOAuthCompletePage() { |
| 32 | | − useEffect(() => { |
| 33 | | − let cancelled = false; |
| 47 | + const sentRef = useRef(false); |
| 48 | + const resultQuery = useTanstackQuery({ |
| 49 | + queryKey: ["mcp-oauth-popup-result"], |
| 50 | + queryFn: fetchPopupResult, |
| 51 | + retry: false, |
| 52 | + staleTime: 0, |
| 53 | + }); |
| 34 | 54 | |
| 35 | | − const finish = async () => { |
| 36 | | − let message: PopupMessage; |
| 37 | | − try { |
| 38 | | − const response = await fetch("/mcp/oauth/result", { |
| 39 | | − method: "GET", |
| 40 | | − cache: "no-store", |
| 41 | | − credentials: "same-origin", |
| 42 | | − }); |
| 43 | | − const data = await response.json() as PopupMessage; |
| 44 | | − if (!data || data.type !== "executor:mcp-oauth-result") { |
| 45 | | − throw new Error("Invalid OAuth completion payload"); |
| 46 | | − } |
| 47 | | − message = data; |
| 48 | | − } catch (error) { |
| 49 | | − message = fallbackErrorMessage(error); |
| 50 | | − } |
| 55 | + useEffect(() => { |
| 56 | + if (sentRef.current || resultQuery.isPending) { |
| 57 | + return; |
| 58 | + } |
| 51 | 59 | |
| 52 | | − if (cancelled) { |
| 53 | | − return; |
| 54 | | − } |
| 60 | + sentRef.current = true; |
| 61 | + const message = resultQuery.isSuccess |
| 62 | + ? resultQuery.data |
| 63 | + : fallbackErrorMessage(resultQuery.error); |
| 55 | 64 | |
| 56 | | − try { |
| 57 | | − if (window.opener) { |
| 58 | | − window.opener.postMessage(message, window.location.origin); |
| 59 | | − } |
| 60 | | − } finally { |
| 61 | | − window.close(); |
| 65 | + try { |
| 66 | + if (window.opener) { |
| 67 | + window.opener.postMessage(message, window.location.origin); |
| 62 | 68 | } |
| 63 | | − }; |
| 64 | | − |
| 65 | | − void finish(); |
| 66 | | − return () => { |
| 67 | | − cancelled = true; |
| 68 | | − }; |
| 69 | | − }, []); |
| 69 | + } finally { |
| 70 | + window.close(); |
| 71 | + } |
| 72 | + }, [resultQuery.data, resultQuery.error, resultQuery.isPending, resultQuery.isSuccess]); |
| 70 | 73 | |
| 71 | 74 | return ( |
| 72 | 75 | <main className="min-h-screen flex items-center justify-center bg-background p-6"> |