[backport cloud/1.35] feat: add pricing table to user popover (#7593)

Backport of #7583 to `cloud/1.35`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7593-backport-cloud-1-35-feat-add-pricing-table-to-user-popover-2cc6d73d365081e4a4d2d7f22068d769)
by [Unito](https://www.unito.io)

Co-authored-by: Simula_r <18093452+simula-r@users.noreply.github.com>
This commit is contained in:
Comfy Org PR Bot
2025-12-18 08:42:56 +09:00
committed by GitHub
parent 6eca4aae86
commit 334511f482
4 changed files with 97 additions and 34 deletions

View File

@@ -18,7 +18,15 @@
</div> </div>
</Button> </Button>
<Popover ref="popover" :show-arrow="false"> <Popover
ref="popover"
:show-arrow="false"
:pt="{
root: {
class: 'rounded-lg'
}
}"
>
<CurrentUserPopover @close="closePopover" /> <CurrentUserPopover @close="closePopover" />
</Popover> </Popover>
</div> </div>

View File

@@ -85,10 +85,24 @@ const mockFetchStatus = vi.fn().mockResolvedValue(undefined)
vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({ vi.mock('@/platform/cloud/subscription/composables/useSubscription', () => ({
useSubscription: vi.fn(() => ({ useSubscription: vi.fn(() => ({
isActiveSubscription: { value: true }, isActiveSubscription: { value: true },
subscriptionTierName: { value: 'Creator' },
subscriptionTier: { value: 'CREATOR' },
fetchStatus: mockFetchStatus fetchStatus: mockFetchStatus
})) }))
})) }))
// Mock the useSubscriptionDialog composable
const mockSubscriptionDialogShow = vi.fn()
vi.mock(
'@/platform/cloud/subscription/composables/useSubscriptionDialog',
() => ({
useSubscriptionDialog: vi.fn(() => ({
show: mockSubscriptionDialogShow,
hide: vi.fn()
}))
})
)
// Mock UserAvatar component // Mock UserAvatar component
vi.mock('@/components/common/UserAvatar.vue', () => ({ vi.mock('@/components/common/UserAvatar.vue', () => ({
default: { default: {
@@ -272,4 +286,22 @@ describe('CurrentUserPopover', () => {
expect(wrapper.emitted('close')).toBeTruthy() expect(wrapper.emitted('close')).toBeTruthy()
expect(wrapper.emitted('close')!.length).toBe(1) expect(wrapper.emitted('close')!.length).toBe(1)
}) })
it('opens subscription dialog and emits close event when plans & pricing item is clicked', async () => {
const wrapper = mountComponent()
const plansPricingItem = wrapper.find(
'[data-testid="plans-pricing-menu-item"]'
)
expect(plansPricingItem.exists()).toBe(true)
await plansPricingItem.trigger('click')
// Verify subscription dialog show was called
expect(mockSubscriptionDialogShow).toHaveBeenCalled()
// Verify close event was emitted
expect(wrapper.emitted('close')).toBeTruthy()
expect(wrapper.emitted('close')!.length).toBe(1)
})
}) })

View File

@@ -21,9 +21,12 @@
<p v-if="userEmail" class="my-0 truncate text-sm text-muted"> <p v-if="userEmail" class="my-0 truncate text-sm text-muted">
{{ userEmail }} {{ userEmail }}
</p> </p>
<p v-if="subscriptionTierName" class="my-0 truncate text-sm text-muted"> <span
v-if="subscriptionTierName"
class="my-0 text-xs text-foreground bg-secondary-background-hover rounded-full uppercase px-2 py-0.5 font-bold mt-2"
>
{{ subscriptionTierName }} {{ subscriptionTierName }}
</p> </span>
</div> </div>
<!-- Credits Section --> <!-- Credits Section -->
@@ -33,11 +36,16 @@
v-if="authStore.isFetchingBalance" v-if="authStore.isFetchingBalance"
width="4rem" width="4rem"
height="1.25rem" height="1.25rem"
class="flex-1" class="w-full"
/> />
<span v-else class="text-base font-normal text-base-foreground flex-1">{{ <span v-else class="text-base font-semibold text-base-foreground">{{
formattedBalance formattedBalance
}}</span> }}</span>
<i
v-if="flags.subscriptionTiersEnabled"
v-tooltip="{ value: $t('credits.unified.tooltip'), showDelay: 300 }"
class="icon-[lucide--circle-help] cursor-help text-base text-muted-foreground mr-auto"
/>
<Button <Button
:label="$t('subscription.addCredits')" :label="$t('subscription.addCredits')"
severity="secondary" severity="secondary"
@@ -58,24 +66,6 @@
/> />
</div> </div>
<!-- Credits info row -->
<div
v-if="flags.subscriptionTiersEnabled && isActiveSubscription"
class="flex items-center gap-2 px-4 py-0"
>
<i
v-tooltip="{
value: $t('credits.unified.tooltip'),
showDelay: 300,
hideDelay: 300
}"
class="icon-[lucide--circle-help] cursor-help text-xs text-muted-foreground"
/>
<span class="text-sm text-muted-foreground">{{
$t('credits.unified.message')
}}</span>
</div>
<Divider class="my-2 mx-0" /> <Divider class="my-2 mx-0" />
<div <div
@@ -91,14 +81,31 @@
</div> </div>
<div <div
v-if="isActiveSubscription"
class="flex items-center gap-2 px-4 py-2 cursor-pointer hover:bg-secondary-background-hover" class="flex items-center gap-2 px-4 py-2 cursor-pointer hover:bg-secondary-background-hover"
data-testid="plan-credits-menu-item" data-testid="plans-pricing-menu-item"
@click="handleOpenPlanAndCreditsSettings" @click="handleOpenPlansAndPricing"
> >
<i class="icon-[lucide--receipt-text] text-muted-foreground text-sm" /> <i class="icon-[lucide--receipt-text] text-muted-foreground text-sm" />
<span class="text-sm text-base-foreground flex-1">{{ <span class="text-sm text-base-foreground flex-1">{{
$t(planSettingsLabel) $t('subscription.plansAndPricing')
}}</span>
<span
v-if="canUpgrade"
class="text-xs font-bold text-base-background bg-base-foreground px-1.5 py-0.5 rounded-full"
>
{{ $t('subscription.upgrade') }}
</span>
</div>
<div
v-if="isActiveSubscription"
class="flex items-center gap-2 px-4 py-2 cursor-pointer hover:bg-secondary-background-hover"
data-testid="manage-plan-menu-item"
@click="handleOpenPlanAndCreditsSettings"
>
<i class="icon-[lucide--file-text] text-muted-foreground text-sm" />
<span class="text-sm text-base-foreground flex-1">{{
$t('subscription.managePlan')
}}</span> }}</span>
</div> </div>
@@ -109,7 +116,7 @@
> >
<i class="icon-[lucide--settings-2] text-muted-foreground text-sm" /> <i class="icon-[lucide--settings-2] text-muted-foreground text-sm" />
<span class="text-sm text-base-foreground flex-1">{{ <span class="text-sm text-base-foreground flex-1">{{
$t('userSettings.title') $t('userSettings.accountSettings')
}}</span> }}</span>
</div> </div>
@@ -143,6 +150,7 @@ import { useExternalLink } from '@/composables/useExternalLink'
import { useFeatureFlags } from '@/composables/useFeatureFlags' import { useFeatureFlags } from '@/composables/useFeatureFlags'
import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue' import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription' import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { useSubscriptionDialog } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
import { isCloud } from '@/platform/distribution/types' import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry' import { useTelemetry } from '@/platform/telemetry'
import { useDialogService } from '@/services/dialogService' import { useDialogService } from '@/services/dialogService'
@@ -154,17 +162,18 @@ const emit = defineEmits<{
const { buildDocsUrl } = useExternalLink() const { buildDocsUrl } = useExternalLink()
const planSettingsLabel = isCloud
? 'settingsCategories.PlanCredits'
: 'settingsCategories.Credits'
const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } = const { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =
useCurrentUser() useCurrentUser()
const authActions = useFirebaseAuthActions() const authActions = useFirebaseAuthActions()
const authStore = useFirebaseAuthStore() const authStore = useFirebaseAuthStore()
const dialogService = useDialogService() const dialogService = useDialogService()
const { isActiveSubscription, subscriptionTierName, fetchStatus } = const {
useSubscription() isActiveSubscription,
subscriptionTierName,
subscriptionTier,
fetchStatus
} = useSubscription()
const subscriptionDialog = useSubscriptionDialog()
const { flags } = useFeatureFlags() const { flags } = useFeatureFlags()
const { locale } = useI18n() const { locale } = useI18n()
@@ -181,11 +190,21 @@ const formattedBalance = computed(() => {
}) })
}) })
const canUpgrade = computed(() => {
const tier = subscriptionTier.value
return tier === 'STANDARD' || tier === 'CREATOR'
})
const handleOpenUserSettings = () => { const handleOpenUserSettings = () => {
dialogService.showSettingsDialog('user') dialogService.showSettingsDialog('user')
emit('close') emit('close')
} }
const handleOpenPlansAndPricing = () => {
subscriptionDialog.show()
emit('close')
}
const handleOpenPlanAndCreditsSettings = () => { const handleOpenPlanAndCreditsSettings = () => {
if (isCloud) { if (isCloud) {
dialogService.showSettingsDialog('subscription') dialogService.showSettingsDialog('subscription')

View File

@@ -2018,6 +2018,9 @@
"contactUs": "Contact us", "contactUs": "Contact us",
"viewEnterprise": "View enterprise", "viewEnterprise": "View enterprise",
"partnerNodesCredits": "Partner nodes pricing", "partnerNodesCredits": "Partner nodes pricing",
"plansAndPricing": "Plans & pricing",
"managePlan": "Manage plan",
"upgrade": "UPGRADE",
"mostPopular": "Most popular", "mostPopular": "Most popular",
"currentPlan": "Current Plan", "currentPlan": "Current Plan",
"subscribeTo": "Subscribe to {plan}", "subscribeTo": "Subscribe to {plan}",
@@ -2046,6 +2049,7 @@
}, },
"userSettings": { "userSettings": {
"title": "My Account Settings", "title": "My Account Settings",
"accountSettings": "Account settings",
"name": "Name", "name": "Name",
"email": "Email", "email": "Email",
"provider": "Sign-in Provider", "provider": "Sign-in Provider",