From 84cfe54ba198a77a0a4af4ea5913ec11adad40c4 Mon Sep 17 00:00:00 2001 From: Gagancreates Date: Mon, 1 Jun 2026 13:22:54 +0530 Subject: [PATCH] feat(google-docs): offer BYOK connect in picker so signed-in users can grant Drive/Docs scopes --- .../components/google-doc-picker-dialog.tsx | 59 ++++++++++++++++--- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/apps/x/apps/renderer/src/components/google-doc-picker-dialog.tsx b/apps/x/apps/renderer/src/components/google-doc-picker-dialog.tsx index baa16dd4..5876e3bd 100644 --- a/apps/x/apps/renderer/src/components/google-doc-picker-dialog.tsx +++ b/apps/x/apps/renderer/src/components/google-doc-picker-dialog.tsx @@ -10,6 +10,8 @@ import { } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' +import { GoogleClientIdModal } from '@/components/google-client-id-modal' +import { setGoogleCredentials } from '@/lib/google-credentials-store' import { formatRelativeTime } from '@/lib/relative-time' import { toast } from '@/lib/toast' @@ -52,6 +54,7 @@ export function GoogleDocPickerDialog({ const [connecting, setConnecting] = useState(false) const [importingId, setImportingId] = useState(null) const [error, setError] = useState(null) + const [byokOpen, setByokOpen] = useState(false) const canList = Boolean(status?.connected && status.hasRequiredScopes) const targetLabel = useMemo(() => targetFolder.replace(/^knowledge\/?/, '') || 'knowledge', [targetFolder]) @@ -114,6 +117,33 @@ export function GoogleDocPickerDialog({ } }, []) + // BYOK: connect Google with the user's own OAuth client. Unlike the managed + // (rowboat) sign-in, this local flow requests the Drive + Docs scopes, so a + // signed-in user can actually grant Docs access without a backend change. + const handleByokSubmit = useCallback((clientId: string, clientSecret: string) => { + setGoogleCredentials(clientId, clientSecret) + setByokOpen(false) + setConnecting(true) + setError(null) + void window.ipc.invoke('oauth:connect', { provider: 'google', clientId, clientSecret }) + .then((result) => { + if (!result.success) setError(result.error ?? 'Failed to start Google connection') + else toast('Finish Google consent in the browser…', 'info') + }) + .catch((err) => setError(err instanceof Error ? err.message : 'Failed to start Google connection')) + .finally(() => setConnecting(false)) + }, []) + + // Re-check scopes as soon as a Google connection completes in the browser. + useEffect(() => { + if (!open) return + const cleanup = window.ipc.on('oauth:didConnect', (event) => { + if (event.provider !== 'google') return + void loadStatus() + }) + return cleanup + }, [open, loadStatus]) + const handleImport = useCallback(async (doc: GoogleDocListItem) => { setImportingId(doc.id) setError(null) @@ -133,6 +163,7 @@ export function GoogleDocPickerDialog({ }, [onImported, onOpenChange, targetFolder]) return ( + <> @@ -159,19 +190,26 @@ export function GoogleDocPickerDialog({ ) : !status.connected || !status.hasRequiredScopes ? (
- {!status.connected - ? 'Connect Google to choose Docs from Drive.' - : 'Reconnect Google so Rowboat can read Drive metadata and edit Google Docs.'} + To choose Google Docs, Rowboat needs Drive + Docs access.
{status.missingScopes.length > 0 && (
Missing scopes: {status.missingScopes.join(', ')}
)} - +
+ + +
+

+ Managed sign-in may not grant Docs access yet. If it keeps asking for scopes, + connect a Google OAuth client (Desktop app) with the Drive API and Docs API enabled. +

) : ( <> @@ -232,5 +270,12 @@ export function GoogleDocPickerDialog({
+ + ) }