import type { FormEvent } from 'react'; import { useState, useEffect } from 'react'; import { toast } from 'sonner'; import { ChevronDown } from 'lucide-react'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Button } from '@/components/ui/button'; import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/collapsible'; import { useClient } from 'hooks/useClient'; import { useTranslation } from '@/lib/i18n'; import { Untranslated } from '@/components/Untranslated'; type GoogleOAuthSettings = { clientId: string; clientSecret: string; }; // The scope strings are Google's own identifiers; only the explanation beside them is ours. const SCOPES = [ { scope: 'gmail.readonly', descriptionKey: 'settings.google.scopeGmail' }, { scope: 'calendar.readonly', descriptionKey: 'settings.google.scopeCalendar' }, ] as const; const SetupGuide = ({ redirectUri }: { redirectUri: string }) => { const [open, setOpen] = useState(false); const { t } = useTranslation(); /* Every quoted Google-console label below is wrapped in . Per docs/localization.md they are identifiers, not prose: they exist to match what the reader sees on Google's screen, and Google localises by the reader's Google account language rather than ours. The sentences are keyed WHOLE, each ending at the label or link it introduces. The first attempt split them around inline labels, which needs "Go to the" and "page." as separate keys — and the Portuguese value of the second one is "." That is the fragmentation smell, so the English was rephrased to put the label at a clause boundary, which reads naturally in both languages. */ const GoogleLink = ({ href, label }: { href: string; label: string }) => ( {label} ); return ( {t('settings.google.guideToggle')}
  1. {t('settings.google.step1.title')}

    {t('settings.google.step1.body')}{' '} {' '} → Create

  2. {t('settings.google.step2.title')}

    {t('settings.google.step2.body')}{' '} {' '} → Enable

    • Gmail API
    • Google Calendar API
  3. {t('settings.google.step3.title')}

    • {t('settings.google.step3.appName')} App name
    • {t('settings.google.step3.supportEmail')}{' '} User support email
    • {t('settings.google.step3.devContact')}{' '} Developer contact information
    • {t('settings.google.step3.save')} Save
  4. {t('settings.google.step4.title')}

    • {t('settings.google.step4.internal')} Internal
    • {t('settings.google.step4.external')} External
  5. {t('settings.google.step5.title')}

    {t('settings.google.step5.body')}{' '} {' '} → Add or remove scopes

      {SCOPES.map((entry) => (
    • {entry.scope} {' '} — {t(entry.descriptionKey)}
    • ))}

    {t('settings.google.step5.note')}

  6. {t('settings.google.step6.title')}

    {t('settings.google.step6.body')}{' '} {' '} → Create OAuth client

    • {t('settings.google.step6.appType')}{' '} Web application
    • {t('settings.google.step6.nameAnything')}
    • {t('settings.google.step6.redirectUris')}{' '} {redirectUri}
    • {t('settings.google.step6.thenClick')} Create
  7. {t('settings.google.step7.title')}

    {t('settings.google.step7.body')}

); }; type VerifyStatus = { valid: boolean; error: string | null } | null; const CredentialStatus = ({ status, isVerifying }: { status: VerifyStatus; isVerifying: boolean }) => { const { t } = useTranslation(); if (isVerifying) { return (
{t('settings.google.verifying')}
); } if (!status) return null; return (
{status.valid ? t('settings.google.valid') : (status.error ?? t('settings.google.invalid'))}
); }; export const GoogleOAuthConfig = () => { const { t } = useTranslation(); const client = useClient(); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [clientId, setClientId] = useState(''); const [clientSecret, setClientSecret] = useState(''); const [verifyStatus, setVerifyStatus] = useState(null); const [isVerifying, setIsVerifying] = useState(false); const verify = () => { setIsVerifying(true); client .get<{ valid: boolean; error: string | null }>('/integrations/google/verify') .then(setVerifyStatus) .catch(() => setVerifyStatus({ valid: false, error: t('settings.google.verifyFailed') })) .finally(() => setIsVerifying(false)); }; useEffect(() => { client .get('/integrations/google/config') .then((data) => { if (data) { setClientId(data.clientId); setClientSecret(data.clientSecret); } }) .catch(() => {}) .finally(() => { setIsLoading(false); }); }, []); // Verify on load if credentials exist useEffect(() => { if (!isLoading && clientId && clientSecret) verify(); }, [isLoading]); const handleSave = async (ev?: FormEvent) => { ev?.preventDefault(); if (isSaving) return; setIsSaving(true); try { await client.put('/integrations/google/config', { clientId: clientId.trim(), clientSecret: clientSecret.trim() }); toast.success(t('settings.google.saved')); verify(); } catch { toast.error(t('settings.google.saveFailed')); } finally { setIsSaving(false); } }; if (isLoading) return null; const redirectUri = `${window.location.origin}/api/integrations/google/callback`; return (
); };