mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 19:21:54 +00:00
feat: add ever-present upgrade button for free-tier users (#9315)
## Summary Add persistent upgrade CTAs for free-tier users: a topbar button and "Upgrade to add credits" replacing "Add Credits" in popovers and settings panels. ## Changes - **What**: - New `TopbarSubscribeButton` component in both GraphCanvas and LinearView topbars, visible only to free-tier users - Profile popover (legacy + workspace): free-tier users see "Upgrade to add credits" instead of "Add Credits", linking directly to the pricing table - Manage Plan settings (legacy + workspace): same replacement — free-tier users see "Upgrade to add credits" instead of "Add Credits" - Paid-tier users retain the original "Add Credits" behavior in all locations - All upgrade buttons go directly to the pricing table (one-step flow) ## Review Focus - The `isFreeTier` conditional gating on the buttons — ensure free-tier users see upgrade CTAs and paid users see normal Add Credits - Layout in Manage Plan panels uses `flex flex-col gap-3` to stack the upgrade button below the usage history link instead of side-by-side ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9315-feat-add-ever-present-upgrade-button-for-free-tier-users-3166d73d365081228cdfe6a67fec6aec) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -634,6 +634,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@utility bg-subscription-gradient {
|
||||||
|
background: var(--color-subscription-button-gradient);
|
||||||
|
}
|
||||||
|
|
||||||
@utility highlight {
|
@utility highlight {
|
||||||
background-color: color-mix(in srgb, currentColor 20%, transparent);
|
background-color: color-mix(in srgb, currentColor 20%, transparent);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
>
|
>
|
||||||
<WorkflowTabs />
|
<WorkflowTabs />
|
||||||
<TopbarBadges />
|
<TopbarBadges />
|
||||||
|
<TopbarSubscribeButton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -140,6 +141,7 @@ import NodePropertiesPanel from '@/components/rightSidePanel/RightSidePanel.vue'
|
|||||||
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
|
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
|
||||||
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
|
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
|
||||||
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
|
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
|
||||||
|
import TopbarSubscribeButton from '@/components/topbar/TopbarSubscribeButton.vue'
|
||||||
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||||
|
|||||||
@@ -46,6 +46,16 @@
|
|||||||
class="icon-[lucide--circle-help] cursor-help text-base text-muted-foreground mr-auto"
|
class="icon-[lucide--circle-help] cursor-help text-base text-muted-foreground mr-auto"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
v-if="isFreeTier"
|
||||||
|
variant="gradient"
|
||||||
|
size="sm"
|
||||||
|
data-testid="upgrade-to-add-credits-button"
|
||||||
|
@click="handleUpgradeToAddCredits"
|
||||||
|
>
|
||||||
|
{{ $t('subscription.upgradeToAddCredits') }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-else
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="text-base-foreground"
|
class="text-base-foreground"
|
||||||
@@ -61,7 +71,7 @@
|
|||||||
:fluid="false"
|
:fluid="false"
|
||||||
:label="$t('subscription.subscribeToComfyCloud')"
|
:label="$t('subscription.subscribeToComfyCloud')"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="gradient"
|
button-variant="gradient"
|
||||||
@subscribed="handleSubscribed"
|
@subscribed="handleSubscribed"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -170,6 +180,7 @@ const settingsDialog = useSettingsDialog()
|
|||||||
const dialogService = useDialogService()
|
const dialogService = useDialogService()
|
||||||
const {
|
const {
|
||||||
isActiveSubscription,
|
isActiveSubscription,
|
||||||
|
isFreeTier,
|
||||||
subscriptionTierName,
|
subscriptionTierName,
|
||||||
subscriptionTier,
|
subscriptionTier,
|
||||||
fetchStatus
|
fetchStatus
|
||||||
@@ -237,6 +248,11 @@ const handleOpenPartnerNodesInfo = () => {
|
|||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleUpgradeToAddCredits = () => {
|
||||||
|
subscriptionDialog.showPricingTable()
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await handleSignOut()
|
await handleSignOut()
|
||||||
emit('close')
|
emit('close')
|
||||||
|
|||||||
25
src/components/topbar/TopbarSubscribeButton.vue
Normal file
25
src/components/topbar/TopbarSubscribeButton.vue
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<template>
|
||||||
|
<Button
|
||||||
|
v-if="isFreeTier"
|
||||||
|
class="mr-2 shrink-0 whitespace-nowrap"
|
||||||
|
variant="gradient"
|
||||||
|
size="sm"
|
||||||
|
data-testid="topbar-subscribe-button"
|
||||||
|
@click="handleClick"
|
||||||
|
>
|
||||||
|
{{ $t('subscription.subscribeForMore') }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
|
import { useBillingContext } from '@/composables/billing/useBillingContext'
|
||||||
|
import { useSubscriptionDialog } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
|
||||||
|
|
||||||
|
const { isFreeTier } = useBillingContext()
|
||||||
|
const subscriptionDialog = useSubscriptionDialog()
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
subscriptionDialog.showPricingTable()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -19,7 +19,9 @@ export const buttonVariants = cva({
|
|||||||
'text-muted-foreground bg-transparent hover:bg-secondary-background-hover',
|
'text-muted-foreground bg-transparent hover:bg-secondary-background-hover',
|
||||||
'destructive-textonly':
|
'destructive-textonly':
|
||||||
'text-destructive-background bg-transparent hover:bg-destructive-background/10',
|
'text-destructive-background bg-transparent hover:bg-destructive-background/10',
|
||||||
'overlay-white': 'bg-white text-gray-600 hover:bg-white/90'
|
'overlay-white': 'bg-white text-gray-600 hover:bg-white/90',
|
||||||
|
gradient:
|
||||||
|
'bg-subscription-gradient text-white border-transparent hover:opacity-90'
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
sm: 'h-6 rounded-sm px-2 py-1 text-xs',
|
sm: 'h-6 rounded-sm px-2 py-1 text-xs',
|
||||||
@@ -47,7 +49,8 @@ const variants = [
|
|||||||
'textonly',
|
'textonly',
|
||||||
'muted-textonly',
|
'muted-textonly',
|
||||||
'destructive-textonly',
|
'destructive-textonly',
|
||||||
'overlay-white'
|
'overlay-white',
|
||||||
|
'gradient'
|
||||||
] as const satisfies Array<ButtonVariants['variant']>
|
] as const satisfies Array<ButtonVariants['variant']>
|
||||||
const sizes = ['sm', 'md', 'lg', 'icon', 'icon-sm'] as const satisfies Array<
|
const sizes = ['sm', 'md', 'lg', 'icon', 'icon-sm'] as const satisfies Array<
|
||||||
ButtonVariants['size']
|
ButtonVariants['size']
|
||||||
|
|||||||
@@ -2292,6 +2292,8 @@
|
|||||||
},
|
},
|
||||||
"subscribeToRun": "Subscribe",
|
"subscribeToRun": "Subscribe",
|
||||||
"subscribeToRunFull": "Subscribe to Run",
|
"subscribeToRunFull": "Subscribe to Run",
|
||||||
|
"subscribeForMore": "Upgrade",
|
||||||
|
"upgradeToAddCredits": "Upgrade to add credits",
|
||||||
"subscribeNow": "Subscribe Now",
|
"subscribeNow": "Subscribe Now",
|
||||||
"subscribeToComfyCloud": "Subscribe to Comfy Cloud",
|
"subscribeToComfyCloud": "Subscribe to Comfy Cloud",
|
||||||
"workspaceNotSubscribed": "This workspace is not on a subscription",
|
"workspaceNotSubscribed": "This workspace is not on a subscription",
|
||||||
|
|||||||
@@ -2,15 +2,7 @@
|
|||||||
<Button
|
<Button
|
||||||
:size
|
:size
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
variant="primary"
|
:variant="buttonVariant === 'gradient' ? 'gradient' : 'primary'"
|
||||||
:style="
|
|
||||||
variant === 'gradient'
|
|
||||||
? {
|
|
||||||
background: 'var(--color-subscription-button-gradient)',
|
|
||||||
color: 'var(--color-white)'
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
"
|
|
||||||
:class="cn('font-bold', fluid && 'w-full')"
|
:class="cn('font-bold', fluid && 'w-full')"
|
||||||
@click="handleSubscribe"
|
@click="handleSubscribe"
|
||||||
>
|
>
|
||||||
@@ -31,13 +23,13 @@ import { cn } from '@/utils/tailwindUtil'
|
|||||||
const {
|
const {
|
||||||
size = 'lg',
|
size = 'lg',
|
||||||
fluid = true,
|
fluid = true,
|
||||||
variant = 'default',
|
buttonVariant = 'default',
|
||||||
label,
|
label,
|
||||||
disabled = false
|
disabled = false
|
||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
label?: string
|
label?: string
|
||||||
size?: 'sm' | 'lg'
|
size?: 'sm' | 'lg'
|
||||||
variant?: 'default' | 'gradient'
|
buttonVariant?: 'default' | 'gradient'
|
||||||
fluid?: boolean
|
fluid?: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|||||||
@@ -5,13 +5,8 @@
|
|||||||
showDelay: 600
|
showDelay: 600
|
||||||
}"
|
}"
|
||||||
class="subscribe-to-run-button whitespace-nowrap"
|
class="subscribe-to-run-button whitespace-nowrap"
|
||||||
variant="primary"
|
variant="gradient"
|
||||||
size="sm"
|
size="sm"
|
||||||
:style="{
|
|
||||||
background: 'var(--color-subscription-button-gradient)',
|
|
||||||
color: 'var(--color-white)',
|
|
||||||
borderColor: 'transparent'
|
|
||||||
}"
|
|
||||||
data-testid="subscribe-to-run-button"
|
data-testid="subscribe-to-run-button"
|
||||||
@click="handleSubscribeToRun"
|
@click="handleSubscribeToRun"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -130,17 +130,25 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex flex-col gap-3">
|
||||||
<a
|
<a
|
||||||
href="https://platform.comfy.org/profile/usage"
|
href="https://platform.comfy.org/profile/usage"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="text-sm underline text-center text-muted"
|
class="text-sm underline text-muted"
|
||||||
>
|
>
|
||||||
{{ $t('subscription.viewUsageHistory') }}
|
{{ $t('subscription.viewUsageHistory') }}
|
||||||
</a>
|
</a>
|
||||||
<Button
|
<Button
|
||||||
v-if="isActiveSubscription"
|
v-if="isActiveSubscription && isFreeTier"
|
||||||
|
variant="gradient"
|
||||||
|
class="p-2 min-h-8 rounded-lg text-sm font-normal w-full"
|
||||||
|
@click="handleUpgradeToAddCredits"
|
||||||
|
>
|
||||||
|
{{ $t('subscription.upgradeToAddCredits') }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-else-if="isActiveSubscription"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
class="p-2 min-h-8 rounded-lg text-sm font-normal text-text-primary bg-interface-menu-component-surface-selected"
|
class="p-2 min-h-8 rounded-lg text-sm font-normal text-text-primary bg-interface-menu-component-surface-selected"
|
||||||
@click="handleAddApiCredits"
|
@click="handleAddApiCredits"
|
||||||
@@ -234,7 +242,8 @@ const {
|
|||||||
isYearlySubscription
|
isYearlySubscription
|
||||||
} = useSubscription()
|
} = useSubscription()
|
||||||
|
|
||||||
const { show: showSubscriptionDialog } = useSubscriptionDialog()
|
const { show: showSubscriptionDialog, showPricingTable } =
|
||||||
|
useSubscriptionDialog()
|
||||||
|
|
||||||
const tierKey = computed(() => {
|
const tierKey = computed(() => {
|
||||||
const tier = subscriptionTier.value
|
const tier = subscriptionTier.value
|
||||||
@@ -296,6 +305,10 @@ const { totalCredits, monthlyBonusCredits, prepaidCredits, isLoadingBalance } =
|
|||||||
|
|
||||||
const { handleAddApiCredits, handleRefresh } = useSubscriptionActions()
|
const { handleAddApiCredits, handleRefresh } = useSubscriptionActions()
|
||||||
|
|
||||||
|
function handleUpgradeToAddCredits() {
|
||||||
|
showPricingTable()
|
||||||
|
}
|
||||||
|
|
||||||
// Focus-based polling: refresh balance when user returns from Stripe checkout
|
// Focus-based polling: refresh balance when user returns from Stripe checkout
|
||||||
const PENDING_TOPUP_KEY = 'pending_topup_timestamp'
|
const PENDING_TOPUP_KEY = 'pending_topup_timestamp'
|
||||||
const TOPUP_EXPIRY_MS = 5 * 60 * 1000 // 5 minutes
|
const TOPUP_EXPIRY_MS = 5 * 60 * 1000 // 5 minutes
|
||||||
|
|||||||
@@ -72,9 +72,19 @@
|
|||||||
v-tooltip="{ value: $t('credits.unified.tooltip'), showDelay: 300 }"
|
v-tooltip="{ value: $t('credits.unified.tooltip'), showDelay: 300 }"
|
||||||
class="icon-[lucide--circle-help] mr-auto cursor-help text-base text-muted-foreground"
|
class="icon-[lucide--circle-help] mr-auto cursor-help text-base text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
<!-- Add Credits (subscribed + personal or workspace owner only) -->
|
<!-- Upgrade to add credits (free tier) -->
|
||||||
<Button
|
<Button
|
||||||
v-if="isActiveSubscription && permissions.canTopUp"
|
v-if="isActiveSubscription && permissions.canTopUp && isFreeTier"
|
||||||
|
variant="gradient"
|
||||||
|
size="sm"
|
||||||
|
data-testid="upgrade-to-add-credits-button"
|
||||||
|
@click="handleUpgradeToAddCredits"
|
||||||
|
>
|
||||||
|
{{ $t('subscription.upgradeToAddCredits') }}
|
||||||
|
</Button>
|
||||||
|
<!-- Add Credits (subscribed + personal or workspace owner only, paid tier) -->
|
||||||
|
<Button
|
||||||
|
v-else-if="isActiveSubscription && permissions.canTopUp"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="text-base-foreground"
|
class="text-base-foreground"
|
||||||
@@ -93,7 +103,7 @@
|
|||||||
: $t('workspaceSwitcher.subscribe')
|
: $t('workspaceSwitcher.subscribe')
|
||||||
"
|
"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="gradient"
|
button-variant="gradient"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
v-if="showSubscribeAction && !isPersonalWorkspace"
|
v-if="showSubscribeAction && !isPersonalWorkspace"
|
||||||
@@ -242,8 +252,14 @@ const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
|
|||||||
useCurrentUser()
|
useCurrentUser()
|
||||||
const settingsDialog = useSettingsDialog()
|
const settingsDialog = useSettingsDialog()
|
||||||
const dialogService = useDialogService()
|
const dialogService = useDialogService()
|
||||||
const { isActiveSubscription, subscription, balance, isLoading, fetchBalance } =
|
const {
|
||||||
useBillingContext()
|
isActiveSubscription,
|
||||||
|
isFreeTier,
|
||||||
|
subscription,
|
||||||
|
balance,
|
||||||
|
isLoading,
|
||||||
|
fetchBalance
|
||||||
|
} = useBillingContext()
|
||||||
|
|
||||||
const isCancelled = computed(() => subscription.value?.isCancelled ?? false)
|
const isCancelled = computed(() => subscription.value?.isCancelled ?? false)
|
||||||
const subscriptionDialog = useSubscriptionDialog()
|
const subscriptionDialog = useSubscriptionDialog()
|
||||||
@@ -310,6 +326,11 @@ const handleOpenPlanAndCreditsSettings = () => {
|
|||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleUpgradeToAddCredits = () => {
|
||||||
|
subscriptionDialog.showPricingTable()
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
const handleTopUp = () => {
|
const handleTopUp = () => {
|
||||||
// Track purchase credits entry from avatar popover
|
// Track purchase credits entry from avatar popover
|
||||||
useTelemetry()?.trackAddApiCreditButtonClicked()
|
useTelemetry()?.trackAddApiCreditButtonClicked()
|
||||||
|
|||||||
@@ -252,9 +252,18 @@
|
|||||||
!showZeroState &&
|
!showZeroState &&
|
||||||
permissions.canTopUp
|
permissions.canTopUp
|
||||||
"
|
"
|
||||||
class="flex items-center justify-between"
|
class="flex flex-col gap-3"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
|
v-if="isFreeTierPlan"
|
||||||
|
variant="gradient"
|
||||||
|
class="p-2 min-h-8 rounded-lg text-sm font-normal w-full"
|
||||||
|
@click="handleUpgradeToAddCredits"
|
||||||
|
>
|
||||||
|
{{ $t('subscription.upgradeToAddCredits') }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-else
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
class="p-2 min-h-8 rounded-lg text-sm font-normal text-text-primary bg-interface-menu-component-surface-selected"
|
class="p-2 min-h-8 rounded-lg text-sm font-normal text-text-primary bg-interface-menu-component-surface-selected"
|
||||||
@click="handleAddApiCredits"
|
@click="handleAddApiCredits"
|
||||||
@@ -464,6 +473,10 @@ function handleSubscribeWorkspace() {
|
|||||||
function handleUpgrade() {
|
function handleUpgrade() {
|
||||||
isFreeTierPlan.value ? showPricingTable() : showSubscriptionDialog()
|
isFreeTierPlan.value ? showPricingTable() : showSubscriptionDialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleUpgradeToAddCredits() {
|
||||||
|
showPricingTable()
|
||||||
|
}
|
||||||
const subscriptionTier = computed(() => subscription.value?.tier ?? null)
|
const subscriptionTier = computed(() => subscription.value?.tier ?? null)
|
||||||
const isYearlySubscription = computed(
|
const isYearlySubscription = computed(
|
||||||
() => subscription.value?.duration === 'ANNUAL'
|
() => subscription.value?.duration === 'ANNUAL'
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import AppModeToolbar from '@/components/appMode/AppModeToolbar.vue'
|
|||||||
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
|
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
|
||||||
import ModeToggle from '@/components/sidebar/ModeToggle.vue'
|
import ModeToggle from '@/components/sidebar/ModeToggle.vue'
|
||||||
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
|
import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
|
||||||
|
import TopbarSubscribeButton from '@/components/topbar/TopbarSubscribeButton.vue'
|
||||||
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
|
||||||
import TypeformPopoverButton from '@/components/ui/TypeformPopoverButton.vue'
|
import TypeformPopoverButton from '@/components/ui/TypeformPopoverButton.vue'
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
@@ -82,6 +83,7 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef')
|
|||||||
<div class="flex h-full items-center">
|
<div class="flex h-full items-center">
|
||||||
<WorkflowTabs />
|
<WorkflowTabs />
|
||||||
<TopbarBadges />
|
<TopbarBadges />
|
||||||
|
<TopbarSubscribeButton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user