mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 10:25:06 +03:00
## What Concludes the `upgradeCtaPlacement` experiment ([#45858](https://github.com/supabase/supabase/pull/45858)) by shipping **both** placements permanently and removing the A/B scaffolding. The experiment tested two upgrade-CTA placements against a control: - **`user_dropdown`** — an "Upgrade to Pro" button in the account/user dropdown - **`org_projects_list`** — a plan-usage card in the org project list Both moved paid conversion in the same direction over control with no activation downside, and they live on separate surfaces, so we're keeping both rather than picking one. ## Changes - **Remove the experiment machinery.** Deleted `useUpgradeCtaExperiment` (PostHog `upgradeCtaPlacement` flag read, `control` arm, variant selection, per-org localStorage seeding, exposure tracking) and replaced it with a small `useShowUpgradeCta` hook that gates purely on **free plan + hosted platform**. Both placements now render for every eligible free-plan org. - **Telemetry cleanup.** Removed the `upgrade_cta_placement_experiment_exposed` event (pure experiment scaffolding). Kept `upgrade_cta_clicked` (with its `placement` property) so we can still measure the CTAs going forward; de-experimented its wording. - **Dead code.** Removed the unused `prependCard` prop plumbing added to `ProjectList` / `EmptyStates` (no caller ever passed it — the org card renders via the `<aside>`). - De-experimented the remaining doc comments in `PlanUsageCard`. Gating is unchanged in spirit: paid orgs and self-hosted never see the CTA. `useShowUpgradeCta` waits until the org plan is known before returning true, so the CTA fades in for free users and never flashes for paid users. ## Testing - `pnpm --filter common typecheck` and `pnpm --filter studio typecheck` pass. - eslint + prettier clean on the changed files. - Manually checked <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Upgrade/usage CTAs now appear only when the organization is confirmed to be on a free plan, avoiding premature rendering before plan data is available. - Upgrade CTA visibility is now consistently driven across supported project and organization surfaces (including the user dropdown) for org-scoped routes. - **Bug Fixes** - Removed obsolete “prepend” placeholders from the project list loading and grid views to keep card layouts consistent. - **Documentation** - Clarified upgrade CTA telemetry wording and adjusted the frontend telemetry contract. - **Tests** - Added coverage for upgrade-CTA visibility behavior across key scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
205 lines
7.0 KiB
TypeScript
205 lines
7.0 KiB
TypeScript
import { FlaskConical, Loader2, ScrollText, User2 } from 'lucide-react'
|
|
import { useTheme } from 'next-themes'
|
|
import Link from 'next/link'
|
|
import { useRouter } from 'next/router'
|
|
import { useState } from 'react'
|
|
import {
|
|
cn,
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuGroup,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuRadioGroup,
|
|
DropdownMenuRadioItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
singleThemes,
|
|
} from 'ui'
|
|
|
|
import { ButtonTooltip } from '../ui/ButtonTooltip'
|
|
import { useFeaturePreviewModal } from './App/FeaturePreview/FeaturePreviewContext'
|
|
import { TimezoneDropdown } from './UserDropdown/TimezoneDropdown'
|
|
import { ProfileImage } from '@/components/ui/ProfileImage'
|
|
import { UpgradePlanButton } from '@/components/ui/UpgradePlanButton'
|
|
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
|
import { useShowUpgradeCta } from '@/hooks/misc/useShowUpgradeCta'
|
|
import { IS_PLATFORM } from '@/lib/constants'
|
|
import { useProfileNameAndPicture } from '@/lib/profile'
|
|
import { useTrack } from '@/lib/telemetry/track'
|
|
import { useAppStateSnapshot } from '@/state/app-state'
|
|
|
|
export function UserDropdown({
|
|
triggerClassName,
|
|
contentClassName,
|
|
}: {
|
|
triggerClassName?: string
|
|
contentClassName?: string
|
|
}) {
|
|
const router = useRouter()
|
|
const { theme, setTheme } = useTheme()
|
|
const appStateSnapshot = useAppStateSnapshot()
|
|
const profileShowEmailEnabled = useIsFeatureEnabled('profile:show_email')
|
|
const { username, avatarUrl, primaryEmail, isLoading } = useProfileNameAndPicture()
|
|
|
|
const { toggleFeaturePreviewModal } = useFeaturePreviewModal()
|
|
const track = useTrack()
|
|
|
|
// The upgrade CTA is org-scoped, so only enable it on routes where an org is in scope.
|
|
// Excludes /account/*, /organizations, /new, marketing routes, etc. Gating the hook here
|
|
// also skips fetching organization data on routes where the CTA can't render.
|
|
const isOrgScopedRoute =
|
|
router.pathname.startsWith('/project/') || router.pathname.startsWith('/org/')
|
|
const { showUpgradeCta } = useShowUpgradeCta({ enabled: isOrgScopedRoute })
|
|
|
|
const [isOpen, setIsOpen] = useState(false)
|
|
|
|
return (
|
|
<DropdownMenu
|
|
open={isOpen}
|
|
onOpenChange={(open) => {
|
|
setIsOpen(open)
|
|
if (open) track('header_user_dropdown_opened')
|
|
}}
|
|
>
|
|
<DropdownMenuTrigger asChild className={cn('border shrink-0 px-3', triggerClassName)}>
|
|
<ButtonTooltip
|
|
variant="default"
|
|
className="[&>span]:flex px-0 py-0 rounded-full overflow-hidden h-8 w-8"
|
|
tooltip={{ content: { text: 'Account settings' } }}
|
|
>
|
|
{isLoading ? (
|
|
<div className="w-full h-full flex items-center justify-center">
|
|
<Loader2 className="animate-spin text-foreground-lighter" size={16} />
|
|
</div>
|
|
) : (
|
|
<ProfileImage alt={username} src={avatarUrl} className="w-8 h-8 rounded-md" />
|
|
)}
|
|
</ButtonTooltip>
|
|
</DropdownMenuTrigger>
|
|
|
|
<DropdownMenuContent side="bottom" align="end" className={contentClassName}>
|
|
{IS_PLATFORM && (
|
|
<>
|
|
<div className="px-2 py-1 flex flex-col gap-0 text-sm">
|
|
{!!username ? (
|
|
<>
|
|
<span title={username} className="w-full text-left text-foreground truncate">
|
|
{username}
|
|
</span>
|
|
{primaryEmail !== username && profileShowEmailEnabled && (
|
|
<span
|
|
title={primaryEmail}
|
|
className="w-full text-left text-foreground-light text-xs truncate"
|
|
>
|
|
{primaryEmail}
|
|
</span>
|
|
)}
|
|
</>
|
|
) : (
|
|
<span title={primaryEmail} className="w-full text-left text-foreground truncate">
|
|
{primaryEmail}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
<DropdownMenuGroup>
|
|
<DropdownMenuItem className="flex gap-2 cursor-pointer" asChild>
|
|
<Link
|
|
href="/account/me"
|
|
onClick={() => {
|
|
if (router.pathname !== '/account/me') {
|
|
appStateSnapshot.setLastRouteBeforeVisitingAccountPage(router.asPath)
|
|
}
|
|
}}
|
|
>
|
|
<User2 size={14} strokeWidth={1.5} className="text-foreground-lighter" />
|
|
Account
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
className="flex gap-2 cursor-pointer"
|
|
onClick={() => toggleFeaturePreviewModal(true)}
|
|
>
|
|
<FlaskConical size={14} strokeWidth={1.5} className="text-foreground-lighter" />
|
|
Feature previews
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem className="flex gap-2 cursor-pointer" asChild>
|
|
<Link
|
|
href="https://supabase.com/changelog"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<ScrollText size={14} strokeWidth={1.5} className="text-foreground-lighter" />
|
|
Changelog
|
|
</Link>
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
</DropdownMenuGroup>
|
|
</>
|
|
)}
|
|
|
|
<DropdownMenuGroup>
|
|
<DropdownMenuLabel>Theme</DropdownMenuLabel>
|
|
<DropdownMenuRadioGroup
|
|
value={theme}
|
|
onValueChange={(value) => {
|
|
setTheme(value)
|
|
}}
|
|
>
|
|
{singleThemes.map((theme) => (
|
|
<DropdownMenuRadioItem
|
|
key={theme.value}
|
|
value={theme.value}
|
|
className="cursor-pointer"
|
|
>
|
|
{theme.name}
|
|
</DropdownMenuRadioItem>
|
|
))}
|
|
</DropdownMenuRadioGroup>
|
|
</DropdownMenuGroup>
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
<DropdownMenuGroup>
|
|
<TimezoneDropdown />
|
|
</DropdownMenuGroup>
|
|
|
|
{showUpgradeCta && (
|
|
<>
|
|
<DropdownMenuSeparator />
|
|
<div className="p-1">
|
|
<UpgradePlanButton
|
|
source="user_dropdown"
|
|
plan="Pro"
|
|
className="w-full justify-center"
|
|
onClick={() => {
|
|
track('upgrade_cta_clicked', { placement: 'user_dropdown' })
|
|
setIsOpen(false)
|
|
}}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
{IS_PLATFORM && (
|
|
<>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuGroup>
|
|
<DropdownMenuItem
|
|
className="cursor-pointer"
|
|
onSelect={() => {
|
|
router.push('/logout')
|
|
}}
|
|
>
|
|
Log out
|
|
</DropdownMenuItem>
|
|
</DropdownMenuGroup>
|
|
</>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)
|
|
}
|