From c9c8c008b2efc5d2fb289e34c2c686556f3b9fa9 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Thu, 4 Dec 2025 16:33:39 -0800 Subject: [PATCH] feat(settings): add Google Workspace integration card MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 of Data Sovereignty Zone implementation: - Add Google Workspace section to Settings > Integrations tab - Show connection status, import counts (emails, files, photos, events) - Connect/Disconnect Google account buttons - "Open Data Browser" button (Phase 2 will implement the browser) - Add getStoredCounts() and getInstance() to GoogleDataService Privacy messaging: "Your data is encrypted with AES-256 and stored only in your browser. Choose what to share to the board." 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/lib/google/index.ts | 45 ++++++++ src/ui/UserSettingsModal.tsx | 200 ++++++++++++++++++++++++++++++++++- 2 files changed, 244 insertions(+), 1 deletion(-) diff --git a/src/lib/google/index.ts b/src/lib/google/index.ts index 4c2944b..678ed10 100644 --- a/src/lib/google/index.ts +++ b/src/lib/google/index.ts @@ -256,6 +256,51 @@ export class GoogleDataService { return await checkStorageQuota(); } + // Get count of items stored for each service + async getStoredCounts(): Promise> { + const counts: Record = { + gmail: 0, + drive: 0, + photos: 0, + calendar: 0, + }; + + try { + const db = await openDatabase(); + if (!db) return counts; + + // Count items in each store + const countStore = async (storeName: string): Promise => { + return new Promise((resolve) => { + try { + const tx = db.transaction(storeName, 'readonly'); + const store = tx.objectStore(storeName); + const countRequest = store.count(); + countRequest.onsuccess = () => resolve(countRequest.result); + countRequest.onerror = () => resolve(0); + } catch { + resolve(0); + } + }); + }; + + counts.gmail = await countStore('gmail'); + counts.drive = await countStore('drive'); + counts.photos = await countStore('photos'); + counts.calendar = await countStore('calendar'); + + } catch (error) { + console.warn('Failed to get stored counts:', error); + } + + return counts; + } + + // Singleton getter + static getInstance(): GoogleDataService { + return getGoogleDataService(); + } + // Schedule periodic touch for Safari private scheduleTouchInterval(): void { // Touch data every 6 hours to prevent 7-day eviction diff --git a/src/ui/UserSettingsModal.tsx b/src/ui/UserSettingsModal.tsx index 8a37742..6d29163 100644 --- a/src/ui/UserSettingsModal.tsx +++ b/src/ui/UserSettingsModal.tsx @@ -4,6 +4,7 @@ import { useDialogs } from "tldraw" import { SettingsDialog } from "./SettingsDialog" import { getFathomApiKey, saveFathomApiKey, removeFathomApiKey, isFathomApiKeyConfigured } from "../lib/fathomApiKey" import { linkEmailToAccount, checkEmailStatus, type LookupResult } from "../lib/auth/cryptidEmailService" +import { GoogleDataService, type GoogleService } from "../lib/google" // AI tool model configurations const AI_TOOLS = [ @@ -144,6 +145,17 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use const [emailLinkLoading, setEmailLinkLoading] = useState(false) const [emailLinkMessage, setEmailLinkMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null) + // Google Data state + const [googleConnected, setGoogleConnected] = useState(false) + const [googleLoading, setGoogleLoading] = useState(false) + const [googleCounts, setGoogleCounts] = useState>({ + gmail: 0, + drive: 0, + photos: 0, + calendar: 0, + }) + const [showGoogleDataBrowser, setShowGoogleDataBrowser] = useState(false) + // Check API key status const checkApiKeys = () => { const settings = localStorage.getItem("openai_api_key") @@ -191,6 +203,27 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use fetchEmailStatus() }, [session.authed, session.username]) + // Check Google connection status when modal opens + useEffect(() => { + const checkGoogleStatus = async () => { + try { + const service = GoogleDataService.getInstance() + const isAuthed = await service.isAuthenticated() + setGoogleConnected(isAuthed) + + if (isAuthed) { + // Get stored item counts + const counts = await service.getStoredCounts() + setGoogleCounts(counts) + } + } catch (error) { + console.warn('Failed to check Google status:', error) + setGoogleConnected(false) + } + } + checkGoogleStatus() + }, []) + // Handle email linking const handleLinkEmail = async () => { if (!emailInput.trim() || !session.username) return @@ -238,6 +271,39 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use } } + // Handle Google connect + const handleGoogleConnect = async () => { + setGoogleLoading(true) + try { + const service = GoogleDataService.getInstance() + // Request all services by default + await service.authenticate(['gmail', 'drive', 'photos', 'calendar']) + setGoogleConnected(true) + // Refresh counts after connection + const counts = await service.getStoredCounts() + setGoogleCounts(counts) + } catch (error) { + console.error('Google connect failed:', error) + } finally { + setGoogleLoading(false) + } + } + + // Handle Google disconnect + const handleGoogleDisconnect = async () => { + try { + const service = GoogleDataService.getInstance() + await service.signOut() + setGoogleConnected(false) + setGoogleCounts({ gmail: 0, drive: 0, photos: 0, calendar: 0 }) + } catch (error) { + console.error('Google disconnect failed:', error) + } + } + + // Calculate total imported items + const totalGoogleItems = Object.values(googleCounts).reduce((a, b) => a + b, 0) + // Handle escape key and click outside useEffect(() => { const handleEscape = (e: KeyboardEvent) => { @@ -767,10 +833,142 @@ export function UserSettingsModal({ onClose, isDarkMode, onToggleDarkMode }: Use )} +
+ + {/* Data Import Section */} +

+ Data Import +

+ + {/* Google Workspace */} +
+
+ 🔐 +
+ Google Workspace +

+ Import Gmail, Drive, Photos & Calendar - encrypted locally +

+
+ + {googleConnected ? 'Connected' : 'Not Connected'} + +
+ + {googleConnected && totalGoogleItems > 0 && ( +
+ {googleCounts.gmail > 0 && ( + + 📧 {googleCounts.gmail} emails + + )} + {googleCounts.drive > 0 && ( + + 📁 {googleCounts.drive} files + + )} + {googleCounts.photos > 0 && ( + + 📷 {googleCounts.photos} photos + + )} + {googleCounts.calendar > 0 && ( + + 📅 {googleCounts.calendar} events + + )} +
+ )} + +

+ Your data is encrypted with AES-256 and stored only in your browser. + Choose what to share to the board. +

+ +
+ {googleConnected ? ( + <> + + + + ) : ( + + )} +
+ + {googleConnected && totalGoogleItems === 0 && ( +

+ No data imported yet. Visit /google to import. +

+ )} +
+ {/* Future Integrations Placeholder */}

- More integrations coming soon: Google Calendar, Notion, and more + More integrations coming soon: Notion, Slack, and more