feat(web): pause Google Drive connect with a notify-me fallback (#1602)

![image.png](https://app.graphite.com/user-attachments/assets/6d7dd2cf-575b-450c-b7bb-c24b8ec834b9.png)

Google is not approving new authorizations while it re-reviews our app, so
every path that starts a new Google connection now shows a PAUSED badge and a
"Notify me" button instead of Connect.

- Existing connections are untouched: sync, file picking and history still work.
- Notify me fires a connector_paused_clicked PostHog event and remembers the
choice in localStorage, so we can pull who to email when the review clears.
- One list in lib/connector-availability.ts drives every surface; removing the
two entries turns Google back on.
This commit is contained in:
MaheshtheDev 2026-08-27 20:20:12 +00:00
parent f11d8c4620
commit 29c43984fe
8 changed files with 248 additions and 11 deletions

View file

@ -19,6 +19,8 @@ import {
} from "lucide-react"
import { useEffect, useState } from "react"
import { toast } from "sonner"
import { connectorPause } from "@/lib/connector-availability"
import { useConnectorNotify } from "@/lib/connector-notify"
import type { z } from "zod"
import { dmSans125ClassName, dmSansClassName } from "@/lib/fonts"
import { cn } from "@lib/utils"
@ -505,7 +507,14 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
},
})
const notify = useConnectorNotify()
// Every connect path funnels here; a `disabled` button would swallow the click.
const handleConnect = (provider: ConnectorProvider) => {
if (connectorPause(provider)) {
notify.request(provider)
return
}
setConnectingProvider(provider)
addConnectionMutation.mutate({
provider,
@ -548,14 +557,32 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
<div className="flex items-center gap-3 flex-1">
<Icon className="size-6 text-[#737373]" />
<div className="space-y-[6px] flex-1">
<p className="text-[16px] font-medium">{config.title}</p>
<div className="flex items-center gap-2">
<p className="text-[16px] font-medium">{config.title}</p>
{connectorPause(provider) && (
<span className="shrink-0 rounded-full bg-[#F5A524]/12 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-[0.08em] text-[#F5A524]">
Paused
</span>
)}
</div>
<p className="text-[16px] text-[#737373]">
{config.description}
</p>
</div>
</div>
<div className="flex items-center gap-2">
{provider === "google-drive" ? (
{connectorPause(provider) ? (
<button
type="button"
onClick={() => notify.request(provider)}
title={connectorPause(provider)?.message}
className="bg-[#14161A] text-[#FAFAFA] text-[14px] font-medium px-3 h-8 rounded-md border border-[rgba(82,89,102,0.3)] hover:bg-[#1B1F24] transition-colors"
>
{notify.isRequested(provider)
? "We'll email you"
: "Notify me"}
</button>
) : provider === "google-drive" ? (
<div className="flex items-center rounded-md overflow-hidden">
<button
type="button"
@ -721,6 +748,10 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
<div className="flex flex-col">
<DropdownMenuItem
onClick={() => {
if (connectorPause("google-drive")) {
notify.request("google-drive")
return
}
setConnectingProvider("google-drive")
addConnectionMutation.mutate({
provider: "google-drive",
@ -741,6 +772,10 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
if (connectorPause("google-drive")) {
notify.request("google-drive")
return
}
setConnectingProvider("google-drive")
addConnectionMutation.mutate({
provider: "google-drive",

View file

@ -43,6 +43,7 @@ import { useIsMobile } from "@hooks/use-mobile"
import { useAuth } from "@lib/auth-context"
import { useProject } from "@/stores"
import { useContainerTags } from "@/hooks/use-container-tags"
import { isConnectorPaused } from "@/lib/connector-availability"
import { DEFAULT_PROJECT_ID } from "@lib/constants"
import {
useQuickNoteDraftReset,
@ -603,6 +604,10 @@ export function AppExperience() {
const handleOpenIntegrations = useCallback(
(integration?: IntegrationParamValue) => {
if (integration && isConnectorPaused(integration)) {
void setViewMode("integrations")
return
}
if (integration === "notion" || integration === "google-drive") {
void setAddDoc("connect")
return

View file

@ -37,13 +37,17 @@ import {
FileText,
Globe,
Info,
Bell,
Loader,
Pause,
Plus,
Search,
X,
Zap,
} from "lucide-react"
import { formatRelativeTime } from "@/components/settings/sync-utils"
import { connectorPause } from "@/lib/connector-availability"
import { useConnectorNotify } from "@/lib/connector-notify"
import { useConnectorAccess } from "@/hooks/use-connector-access"
import { useConnectionHealth } from "@/hooks/use-connection-health"
import { useContainerTags } from "@/hooks/use-container-tags"
@ -599,6 +603,52 @@ export function DetailWrapper({
)
}
function NotifyMeButton({
provider,
title,
onClick,
}: {
provider: string
title?: string
onClick?: () => void
}) {
const { isRequested, request } = useConnectorNotify()
const requested = isRequested(provider)
return (
<PillButton
className={requested ? "cursor-default opacity-60" : undefined}
onClick={() => {
onClick?.()
if (!requested) request(provider)
}}
title={title}
>
{requested ? (
<>
<Check className="size-3.5" /> We&apos;ll email you
</>
) : (
<>
<Bell className="size-3.5" /> Notify me
</>
)}
</PillButton>
)
}
function PausedChip() {
return (
<span
className={cn(
dmSans125ClassName(),
"shrink-0 rounded-full bg-[#F5A524]/12 px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-[0.08em] text-[#F5A524]",
)}
>
Paused
</span>
)
}
function ProChip({ children = "Pro" }: { children?: ReactNode }) {
return (
<span
@ -2276,6 +2326,7 @@ function ItemCard({
leftIndicator,
statusSlot,
layoutClassName,
paused,
}: {
actionSlot: ReactNode
infoActionSlot?: ReactNode
@ -2291,6 +2342,7 @@ function ItemCard({
leftIndicator?: ReactNode
statusSlot?: ReactNode
layoutClassName?: string
paused?: boolean
}) {
const [infoOpen, setInfoOpen] = useState(false)
return (
@ -2345,7 +2397,8 @@ function ItemCard({
>
{name}
</span>
{isNew && <NewChip />}
{paused && <PausedChip />}
{isNew && !paused && <NewChip />}
{max ? <ProChip>Max</ProChip> : pro && <ProChip />}
</div>
<p
@ -2979,6 +3032,12 @@ export function IntegrationsView({
}
}
const handlePausedConnector = useCallback((provider: string) => {
const pause = connectorPause(provider)
if (!pause) return
toast.info(pause.message)
}, [])
const handleUpgrade = useCallback(
async (planId?: unknown) => {
const checkoutPlanId = planId === "api_max" ? "api_max" : "api_pro"
@ -3084,6 +3143,10 @@ export function IntegrationsView({
if (["notion", "google-drive", "onedrive"].includes(target)) {
// The add-document modal is driven by its own ?add param, so clearing ?connect is safe.
void setConnectTarget(null)
if (connectorPause(target)) {
handlePausedConnector(target)
return
}
void setAddDoc("connect")
}
}, 0)
@ -3103,6 +3166,7 @@ export function IntegrationsView({
setAddDoc,
handleUpgrade,
openPluginSetup,
handlePausedConnector,
])
const closeMcpModal = () => {
@ -3551,15 +3615,20 @@ export function IntegrationsView({
const count = connectionsByProvider[item.provider].length
const isGranola = item.provider === "granola"
const needsPlanUpgrade = !isAutumnLoading && !connectorAccess
const pause = connectorPause(item.provider)
if (count > 0) {
return (
<div className="flex w-full items-center justify-between gap-2">
<button
type="button"
aria-label="Add another knowledge source"
title="Add another knowledge source"
aria-label={pause ? "Paused" : "Add another knowledge source"}
title={pause ? pause.message : "Add another knowledge source"}
onClick={() => {
trackCard(item)
if (pause) {
handlePausedConnector(item.provider)
return
}
if (isGranola) {
if (!connectorAccess) {
handleUpgrade("api_pro")
@ -3573,13 +3642,27 @@ export function IntegrationsView({
className={cn(
"flex size-8 shrink-0 items-center justify-center rounded-full bg-[#0D121A] text-[#A1A1AA] transition-colors hover:text-[#FAFAFA] sm:size-9",
"shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)]",
pause && "cursor-not-allowed opacity-50",
)}
>
<Plus className="size-4" />
{pause ? (
<Pause className="size-4" />
) : (
<Plus className="size-4" />
)}
</button>
</div>
)
}
if (pause) {
return (
<NotifyMeButton
onClick={() => trackCard(item)}
provider={item.provider}
title={pause.message}
/>
)
}
if (needsPlanUpgrade) {
return (
<PillButton onClick={() => handleUpgrade("api_pro")}>
@ -3795,6 +3878,7 @@ export function IntegrationsView({
leftIndicator={renderLeftIndicator(item)}
statusSlot={renderStatus(item)}
layoutClassName={layoutClassName}
paused={item.kind === "connector" && !!connectorPause(item.provider)}
/>
)

View file

@ -16,17 +16,22 @@ export function PillButton({
onClick,
disabled,
type = "button",
className,
title,
}: {
children: ReactNode
onClick?: () => void
disabled?: boolean
type?: "button" | "submit"
className?: string
title?: string
}) {
return (
<button
type={type}
onClick={onClick}
disabled={disabled}
title={title}
className={cn(
dmSans125ClassName(),
"relative flex h-8 min-w-[94px] shrink-0 items-center justify-center gap-1.5 rounded-full bg-[#0D121A] px-3 sm:h-9 sm:min-w-[116px] sm:px-5",
@ -34,6 +39,7 @@ export function PillButton({
"shadow-[inset_1.5px_1.5px_4.5px_rgba(0,0,0,0.7)]",
"cursor-pointer transition-opacity hover:opacity-80",
"disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
>
{children}

View file

@ -80,6 +80,8 @@ import {
} from "@lib/constants"
import { useCustomer } from "autumn-js/react"
import { toast } from "sonner"
import { connectorPause } from "@/lib/connector-availability"
import { useConnectorNotify } from "@/lib/connector-notify"
import { analytics } from "@/lib/analytics"
import type { BrainMode } from "./types"
import { usePromoCode } from "@/hooks/use-promo-code"
@ -374,6 +376,8 @@ export function StepSources({
return !connectorAccess
}
const notify = useConnectorNotify()
const setState = (id: SourceId, state: SourceState) => {
onChange({ ...values, connected: { ...values.connected, [id]: state } })
}
@ -383,6 +387,11 @@ export function StepSources({
id: SourceId,
) => {
analytics.onboardingIntegrationClicked({ integration: provider })
if (connectorPause(provider)) {
notify.request(provider)
setState(id, "waitlist")
return
}
setState(id, "connecting")
try {
const metadata: Record<string, string> = {}
@ -422,12 +431,18 @@ export function StepSources({
setState(id, "waitlist")
}
// Paused beats locked: upgrading cannot unlock a connector nobody can connect.
const guard = (
plan: RequiredPlan | undefined,
title: string,
fn: () => void,
provider?: string,
) => {
return () => {
if (provider && connectorPause(provider)) {
fn()
return
}
if (isLocked(plan) && plan) {
setRequestedPlan(plan)
setRequestedConnector(title)
@ -948,19 +963,22 @@ function GoogleDriveSourceCard({
plan: RequiredPlan | undefined,
title: string,
fn: () => void,
provider?: string,
) => () => void
connectRealProvider: (
provider: "google-drive" | "notion" | "onedrive",
id: SourceId,
) => void
}) {
const pause = connectorPause("google-drive")
return (
<SourceCard
title="Google Drive"
blurb="Docs, sheets, slides — the working memory of your team."
icon={<GoogleDrive className="size-7" />}
state={values.connected.drive ?? "idle"}
ctaLabel="Connect"
ctaLabel={pause ? "Notify me" : "Connect"}
locked={isLocked("pro")}
requiredPlan="pro"
perks={[
@ -968,11 +986,19 @@ function GoogleDriveSourceCard({
"Stays in sync as files change",
"You pick what to share at sign-in",
]}
onConnect={guard("pro", "Google Drive", () =>
connectRealProvider("google-drive", "drive"),
onConnect={guard(
"pro",
"Google Drive",
() => connectRealProvider("google-drive", "drive"),
"google-drive",
)}
headerNote={
values.driveScope === "full" ? (
pause ? (
<p className="mt-1.5 flex items-center gap-1.5 text-[11px] text-[#F5A524] font-medium">
<AlertTriangle className="size-3 shrink-0" />
{pause.message}
</p>
) : values.driveScope === "full" ? (
<p className="mt-1.5 flex items-center gap-1.5 text-[11px] text-[#FF8A47] font-medium">
<AlertTriangle className="size-3 shrink-0" />
Full Drive can exhaust your monthly usage.
@ -1006,6 +1032,7 @@ function NotionSourceCard({
plan: RequiredPlan | undefined,
title: string,
fn: () => void,
provider?: string,
) => () => void
connectRealProvider: (
provider: "google-drive" | "notion" | "onedrive",
@ -1048,6 +1075,7 @@ function GranolaSourceCard({
plan: RequiredPlan | undefined,
title: string,
fn: () => void,
provider?: string,
) => () => void
onOpen: () => void
}) {
@ -1092,6 +1120,7 @@ function MoreSourcesGrid({
plan: RequiredPlan | undefined,
title: string,
fn: () => void,
provider?: string,
) => () => void
openExternal: (id: SourceId, url: string) => void
requestWaitlist: (id: SourceId) => void
@ -1283,7 +1312,9 @@ function SourceCard({
{isDone ? (
<span className="inline-flex items-center gap-1.5 rounded-full border border-[#2261CA55] bg-[#2261CA1A] px-2.5 py-1 text-[12px] font-semibold text-[#4BA0FA] shrink-0 mt-0.5">
<Check className="size-3.5" />
{state === "waitlist" ? "Requested" : (doneLabel ?? "Connected")}
{state === "waitlist"
? "We'll email you"
: (doneLabel ?? "Connected")}
</span>
) : (
<Button

View file

@ -84,6 +84,8 @@ export const analytics = {
connectionDeleted: () => safeCapture("connection_deleted"),
connectionAuthStarted: (props: { provider: string }) =>
safeCapture("connection_auth_started", props),
connectorPausedClicked: (props: { provider: string; reason: string }) =>
safeCapture("connector_paused_clicked", props),
// integrations surface (main Nova page)
integrationCardClicked: (props: { kind: string; id: string; name: string }) =>

View file

@ -0,0 +1,30 @@
export type ConnectorPauseReason = "google_verification"
export interface ConnectorPause {
reason: ConnectorPauseReason
label: string
message: string
}
const PAUSED_CONNECTORS: Record<string, ConnectorPause> = {
"google-drive": {
reason: "google_verification",
label: "Google Drive",
message:
"New Google Drive connections are paused while Google reviews our app. Already connected? Your files keep syncing.",
},
gmail: {
reason: "google_verification",
label: "Gmail",
message:
"New Gmail connections are paused while Google reviews our app. Already connected? Your email keeps syncing.",
},
}
export function connectorPause(provider: string): ConnectorPause | undefined {
return PAUSED_CONNECTORS[provider]
}
export function isConnectorPaused(provider: string): boolean {
return provider in PAUSED_CONNECTORS
}

View file

@ -0,0 +1,44 @@
"use client"
import { useCallback, useEffect, useState } from "react"
import { toast } from "sonner"
import { analytics } from "@/lib/analytics"
import { connectorPause } from "@/lib/connector-availability"
const key = (provider: string) => `connector_notify:${provider}`
// Per-browser only. The PostHog event is the record of truth for who to email.
export function useConnectorNotify() {
const [requested, setRequested] = useState<Record<string, boolean>>({})
useEffect(() => {
try {
const seen: Record<string, boolean> = {}
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i)
if (k?.startsWith("connector_notify:")) {
seen[k.slice("connector_notify:".length)] = true
}
}
setRequested(seen)
} catch {}
}, [])
const isRequested = useCallback(
(provider: string) => requested[provider] === true,
[requested],
)
const request = useCallback((provider: string) => {
const pause = connectorPause(provider)
if (!pause) return
analytics.connectorPausedClicked({ provider, reason: pause.reason })
try {
localStorage.setItem(key(provider), "1")
} catch {}
setRequested((prev) => ({ ...prev, [provider]: true }))
toast.success(`We'll email you when ${pause.label} is back.`)
}, [])
return { isRequested, request }
}