From 68a761a9032f7fbe268181e1a40976b8183cf429 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 10 Mar 2026 23:20:43 -0700 Subject: [PATCH 1/3] Add UI for redirect URIs to plans --- static/plans/src/App.tsx | 81 ++++++++++ .../plans/src/components/DeveloperAppCard.tsx | 151 ++++++++++++++++++ 2 files changed, 232 insertions(+) diff --git a/static/plans/src/App.tsx b/static/plans/src/App.tsx index 6e3a4090..00b6056b 100644 --- a/static/plans/src/App.tsx +++ b/static/plans/src/App.tsx @@ -696,6 +696,85 @@ export default function App() { [oauthUser?.userId] ); + const handleAddRedirectUri = useCallback( + async (app: DeveloperApp, uri: string) => { + if (oauthUser?.userId == null) throw new Error("Not logged in"); + const token = sessionStorage.getItem(OAUTH_TOKEN_KEY); + const updatedUris = [...(app.redirect_uris ?? [])]; + if (!updatedUris.includes(uri)) updatedUris.push(uri); + const res = await fetch( + `${API_BASE}/v1/developer-apps/${encodeURIComponent( + app.address + )}?user_id=${oauthUser.userId}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + ...(token != null ? { Authorization: `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ + name: app.name, + description: app.description ?? undefined, + image_url: app.image_url ?? undefined, + redirect_uris: updatedUris, + }), + } + ); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error( + (err as { error?: string })?.error ?? "Failed to add redirect URI" + ); + } + setDeveloperApps((prev) => + prev.map((a) => { + if (a.address?.toLowerCase() !== app.address?.toLowerCase()) return a; + return { ...a, redirect_uris: updatedUris }; + }) + ); + }, + [oauthUser?.userId] + ); + + const handleRemoveRedirectUri = useCallback( + async (app: DeveloperApp, uri: string) => { + if (oauthUser?.userId == null) throw new Error("Not logged in"); + const token = sessionStorage.getItem(OAUTH_TOKEN_KEY); + const updatedUris = (app.redirect_uris ?? []).filter((u) => u !== uri); + const res = await fetch( + `${API_BASE}/v1/developer-apps/${encodeURIComponent( + app.address + )}?user_id=${oauthUser.userId}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + ...(token != null ? { Authorization: `Bearer ${token}` } : {}), + }, + body: JSON.stringify({ + name: app.name, + description: app.description ?? undefined, + image_url: app.image_url ?? undefined, + redirect_uris: updatedUris, + }), + } + ); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error( + (err as { error?: string })?.error ?? "Failed to remove redirect URI" + ); + } + setDeveloperApps((prev) => + prev.map((a) => { + if (a.address?.toLowerCase() !== app.address?.toLowerCase()) return a; + return { ...a, redirect_uris: updatedUris }; + }) + ); + }, + [oauthUser?.userId] + ); + const handleDeleteApp = useCallback( async (app: { address: string; name: string }) => { if (oauthUser?.userId == null) throw new Error("Not logged in"); @@ -1309,6 +1388,8 @@ export default function App() { onDelete={setDeleteAppModalApp} onDeactivateAccessKey={handleDeactivateAccessKey} onAddAccessKey={handleAddAccessKey} + onAddRedirectUri={handleAddRedirectUri} + onRemoveRedirectUri={handleRemoveRedirectUri} /> ))} diff --git a/static/plans/src/components/DeveloperAppCard.tsx b/static/plans/src/components/DeveloperAppCard.tsx index 5f3b5496..172adf45 100644 --- a/static/plans/src/components/DeveloperAppCard.tsx +++ b/static/plans/src/components/DeveloperAppCard.tsx @@ -29,6 +29,7 @@ export type DeveloperApp = { request_count_all_time?: number; is_legacy?: boolean; api_access_keys?: ApiAccessKey[]; + redirect_uris?: string[]; }; const messages = { @@ -44,6 +45,10 @@ const messages = { deleteApp: "Delete API Key", revokeToken: "Revoke Bearer Token", newBearerToken: "New Bearer Token", + redirectUrisLabel: "Redirect URIs", + removeUri: "Remove Redirect URI", + addUri: "Add Redirect URI", + addUriPlaceholder: "https://example.com/callback", }; const PLACEHOLDER_COLORS = [ @@ -266,13 +271,149 @@ function BearerTokenField({ ); } +function RedirectUriRow({ + value, + onRemove, +}: { + value: string; + onRemove: () => void | Promise; +}) { + const [pending, setPending] = useState(false); + + const handleRemove = async () => { + if (pending) return; + setPending(true); + try { + await onRemove(); + } catch (err) { + console.error(err); + } finally { + setPending(false); + } + }; + + return ( + + + + + + + ); +} + +function AddRedirectUriRow({ + onAdd, +}: { + onAdd: (uri: string) => void | Promise; +}) { + const [value, setValue] = useState(""); + const [pending, setPending] = useState(false); + const handleAdd = async () => { + if (pending || !value.trim()) return; + setPending(true); + try { + await onAdd(value.trim()); + setValue(""); + } catch (err) { + console.error(err); + } finally { + setPending(false); + } + }; + + return ( + + setValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") void handleAdd(); + }} + css={css` + flex: 1; + padding: 0.5rem 0.75rem; + font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace; + font-size: 0.875rem; + border: 1px solid var(--harmony-neutral-neutral-3, #e0e0e0); + border-radius: 4px; + background: white; + min-width: 0; + `} + /> + + + + + ); +} + +function RedirectUrisField({ + uris, + appAddress, + onRemove, + onAdd, +}: { + uris: string[]; + appAddress: string; + onRemove: (uri: string) => void | Promise; + onAdd: (uri: string) => void | Promise; +}) { + return ( + + + {messages.redirectUrisLabel} + + {uris.map((uri) => ( + onRemove(uri)} + /> + ))} + + + ); +} type Props = { app: DeveloperApp; onDelete: (app: DeveloperApp) => void; onDeactivateAccessKey: (app: DeveloperApp, apiAccessKey: string) => void; onAddAccessKey: (app: DeveloperApp) => void; + onAddRedirectUri: (app: DeveloperApp, uri: string) => void; + onRemoveRedirectUri: (app: DeveloperApp, uri: string) => void; }; export function DeveloperAppCard({ @@ -280,6 +421,8 @@ export function DeveloperAppCard({ onDelete, onDeactivateAccessKey, onAddAccessKey, + onAddRedirectUri, + onRemoveRedirectUri, }: Props) { return ( @@ -410,6 +553,14 @@ export function DeveloperAppCard({ /> )) ?? null : null} + {!app.is_legacy ? ( + onRemoveRedirectUri(app, uri)} + onAdd={(uri) => onAddRedirectUri(app, uri)} + /> + ) : null} From 6ed2e46382e0894058ee8668d3cfce1ac15d165f Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Wed, 11 Mar 2026 00:24:48 -0700 Subject: [PATCH 2/3] Don't disable 'Create API Key' --- static/plans/src/App.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/static/plans/src/App.tsx b/static/plans/src/App.tsx index 00b6056b..6cbf62dc 100644 --- a/static/plans/src/App.tsx +++ b/static/plans/src/App.tsx @@ -1245,8 +1245,9 @@ export default function App() {