feat(mail): withhold new Gmail consents on hosted unless the company is allowlisted (#2320)
Every Gmail consent shows "Google hasn't verified this app" until the restricted-scope review closes, and a prospect bounced on it today. Jakob's call: remove the connector in the meantime rather than explain the screen. New consents are gated by GOOGLE_MAIL_CONNECT_COMPANY_IDS on hosted: unset means nobody (the default from this deploy on), `*` means everybody (set once Google approves), a comma list means those companies (the reviewer's demo company, the company the video is recorded in). Enforced in /oauth/start (403 connect_disabled) and mirrored as connectEnabled on /connections, so the settings page drops its connect button and the inbox start card falls back to plain upload. Existing mailboxes stay listed, keep being searched and can be disconnected. Self-hosted installs run their own Google app and are never gated. Claude-Session: https://claude.ai/code/session_01UD3HsDX8hnJEqpt35azxBJ Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Fable 5.1
parent
3e5670f978
commit
c0eda46354
@@ -670,6 +670,7 @@ export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) {
|
||||
// the mailboxes we search, WhatsApp for photographed receipts, and the
|
||||
// forwarding address that works with nothing connected at all.
|
||||
const [mailConnections, setMailConnections] = useState<InboxMailConnection[]>([])
|
||||
const [mailConnectEnabled, setMailConnectEnabled] = useState(false)
|
||||
const [whatsapp, setWhatsapp] = useState<{ linked: boolean; phoneMasked?: string; verifiedAt?: string | null } | null>(null)
|
||||
const [sourcesOpen, setSourcesOpen] = useState(false)
|
||||
// Received-mail history (#2181): read when its panel is first opened, so
|
||||
@@ -701,8 +702,13 @@ export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) {
|
||||
try {
|
||||
const res = await fetch('/api/extensions/ext/mail/connections')
|
||||
if (!res.ok) return
|
||||
const json = (await res.json()) as { data?: { connections?: InboxMailConnection[] } }
|
||||
const json = (await res.json()) as {
|
||||
data?: { connections?: InboxMailConnection[]; connectEnabled?: boolean }
|
||||
}
|
||||
setMailConnections(json.data?.connections ?? [])
|
||||
// While Google's scope review keeps new consents withheld, the start
|
||||
// card must not send people to a settings page with no connect button.
|
||||
setMailConnectEnabled(json.data?.connectEnabled === true)
|
||||
} catch {
|
||||
// The extension may not be enabled at all; stay quiet.
|
||||
}
|
||||
@@ -1932,11 +1938,16 @@ export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) {
|
||||
dense
|
||||
title={tStart('inbox_title')}
|
||||
body={tStart('inbox_body')}
|
||||
primary={{ label: tStart('inbox_primary'), href: '/settings/mail' }}
|
||||
secondary={{
|
||||
label: tStart('inbox_secondary'),
|
||||
onClick: () => fileInputRef.current?.click(),
|
||||
}}
|
||||
primary={
|
||||
mailConnectEnabled
|
||||
? { label: tStart('inbox_primary'), href: '/settings/mail' }
|
||||
: { label: tStart('inbox_secondary'), onClick: () => fileInputRef.current?.click() }
|
||||
}
|
||||
secondary={
|
||||
mailConnectEnabled
|
||||
? { label: tStart('inbox_secondary'), onClick: () => fileInputRef.current?.click() }
|
||||
: undefined
|
||||
}
|
||||
onDismiss={handleDismissOnboarding}
|
||||
dismissLabel={tStart('inbox_dismiss')}
|
||||
/>
|
||||
@@ -2111,11 +2122,16 @@ export default function InvoiceInboxWorkspace(_props: WorkspaceComponentProps) {
|
||||
floatIcons
|
||||
title={tStart('inbox_title')}
|
||||
body={tStart('inbox_body')}
|
||||
primary={{ label: tStart('inbox_primary'), href: '/settings/mail' }}
|
||||
secondary={{
|
||||
label: tStart('inbox_secondary'),
|
||||
onClick: () => fileInputRef.current?.click(),
|
||||
}}
|
||||
primary={
|
||||
mailConnectEnabled
|
||||
? { label: tStart('inbox_primary'), href: '/settings/mail' }
|
||||
: { label: tStart('inbox_secondary'), onClick: () => fileInputRef.current?.click() }
|
||||
}
|
||||
secondary={
|
||||
mailConnectEnabled
|
||||
? { label: tStart('inbox_secondary'), onClick: () => fileInputRef.current?.click() }
|
||||
: undefined
|
||||
}
|
||||
onDismiss={handleDismissOnboarding}
|
||||
dismissLabel={tStart('inbox_dismiss')}
|
||||
/>
|
||||
|
||||
@@ -33,6 +33,9 @@ export function MailConnectionsPanel() {
|
||||
const t = useTranslations('mail')
|
||||
const [connections, setConnections] = useState<MailConnection[]>([])
|
||||
const [configured, setConfigured] = useState(true)
|
||||
// False while Google's scope review keeps new consents withheld on hosted:
|
||||
// the connect button is simply absent, existing mailboxes stay listed.
|
||||
const [connectEnabled, setConnectEnabled] = useState(false)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [connecting, setConnecting] = useState(false)
|
||||
const [pendingDisconnect, setPendingDisconnect] = useState<MailConnection | null>(null)
|
||||
@@ -45,10 +48,11 @@ export function MailConnectionsPanel() {
|
||||
const response = await fetch(`${BASE}/connections`)
|
||||
if (!response.ok) return
|
||||
const body = (await response.json()) as {
|
||||
data: { connections: MailConnection[]; configured: boolean }
|
||||
data: { connections: MailConnection[]; configured: boolean; connectEnabled?: boolean }
|
||||
}
|
||||
setConnections(body.data.connections)
|
||||
setConfigured(body.data.configured)
|
||||
setConnectEnabled(body.data.connectEnabled === true)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -179,19 +183,23 @@ export function MailConnectionsPanel() {
|
||||
</SettingsGroup>
|
||||
) : null}
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button onClick={connect} disabled={connecting || !configured}>
|
||||
{connecting ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<GoogleMark className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{t('connect')}
|
||||
</Button>
|
||||
{!configured ? <SettingsRowNote>{t('not_configured')}</SettingsRowNote> : null}
|
||||
</div>
|
||||
{connectEnabled ? (
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button onClick={connect} disabled={connecting || !configured}>
|
||||
{connecting ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<GoogleMark className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{t('connect')}
|
||||
</Button>
|
||||
{!configured ? <SettingsRowNote>{t('not_configured')}</SettingsRowNote> : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<p className="max-w-[62ch] text-xs text-muted-foreground">{t('promise')}</p>
|
||||
{connectEnabled || connections.length > 0 ? (
|
||||
<p className="max-w-[62ch] text-xs text-muted-foreground">{t('promise')}</p>
|
||||
) : null}
|
||||
|
||||
<ConfirmDialog
|
||||
open={pendingDisconnect !== null}
|
||||
|
||||
Reference in New Issue
Block a user