update user profile dropdown (#6475)

## Summary

update user profile dropdown

## Screenshots

1. Subscribe to run button and Unsubscribe user panel
<img width="433" height="480" alt="image"
src="https://github.com/user-attachments/assets/bb859481-6405-44df-85ec-9935599c4be0"
/>

2. Subscribed User:
<img width="395" height="479" alt="image"
src="https://github.com/user-attachments/assets/683de2c0-8090-4e9a-ac4e-d211fcee8921"
/>

3. OSS:
<img width="392" height="480" alt="image"
src="https://github.com/user-attachments/assets/7d684c1a-8dee-48dd-8e7f-3c98bd98104d"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6475-update-user-profile-dropdown-29d6d73d365081ff9e14f9355a9a3bb7)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Terry Jia
2025-10-31 17:45:02 -04:00
committed by GitHub
parent afa10f7a1e
commit e05e988730
7 changed files with 127 additions and 51 deletions

View File

@@ -157,6 +157,8 @@
--button-surface: var(--color-white); --button-surface: var(--color-white);
--button-surface-contrast: var(--color-black); --button-surface-contrast: var(--color-black);
--subscription-button-gradient: linear-gradient(315deg, rgb(105 230 255 / 0.15) 0%, rgb(99 73 233 / 0.50) 100%), radial-gradient(70.71% 70.71% at 50% 50%, rgb(62 99 222 / 0.15) 0.01%, rgb(66 0 123 / 0.50) 100%), linear-gradient(92deg, #D000FF 0.38%, #B009FE 37.07%, #3E1FFC 65.17%, #009DFF 103.86%), var(--color-button-surface, #2D2E32);
--modal-card-button-surface: var(--color-smoke-300); --modal-card-button-surface: var(--color-smoke-300);
/* Code styling colors for help menu*/ /* Code styling colors for help menu*/
@@ -258,6 +260,8 @@
--button-active-surface: var(--color-charcoal-600); --button-active-surface: var(--color-charcoal-600);
--button-icon: var(--color-smoke-800); --button-icon: var(--color-smoke-800);
--subscription-button-gradient: linear-gradient(315deg, rgb(105 230 255 / 0.15) 0%, rgb(99 73 233 / 0.50) 100%), radial-gradient(70.71% 70.71% at 50% 50%, rgb(62 99 222 / 0.15) 0.01%, rgb(66 0 123 / 0.50) 100%), linear-gradient(92deg, #D000FF 0.38%, #B009FE 37.07%, #3E1FFC 65.17%, #009DFF 103.86%), var(--color-button-surface, #2D2E32);
--modal-card-button-surface: var(--color-charcoal-300); --modal-card-button-surface: var(--color-charcoal-300);
--dialog-surface: var(--color-neutral-700); --dialog-surface: var(--color-neutral-700);
@@ -332,6 +336,7 @@
--color-button-icon: var(--button-icon); --color-button-icon: var(--button-icon);
--color-button-surface: var(--button-surface); --color-button-surface: var(--button-surface);
--color-button-surface-contrast: var(--button-surface-contrast); --color-button-surface-contrast: var(--button-surface-contrast);
--color-subscription-button-gradient: var(--subscription-button-gradient);
--color-modal-card-button-surface: var(--modal-card-button-surface); --color-modal-card-button-surface: var(--modal-card-button-surface);
--color-dialog-surface: var(--dialog-surface); --color-dialog-surface: var(--dialog-surface);
--color-interface-menu-component-surface-hovered: var( --color-interface-menu-component-surface-hovered: var(

View File

@@ -79,9 +79,11 @@ vi.mock('@/stores/firebaseAuthStore', () => ({
})) }))
// Mock the useSubscription composable // Mock the useSubscription composable
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: vi.fn().mockReturnValue(true) isActiveSubscription: { value: true },
fetchStatus: mockFetchStatus
})) }))
})) }))
@@ -105,6 +107,15 @@ vi.mock('@/components/common/UserCredit.vue', () => ({
} }
})) }))
vi.mock('@/platform/cloud/subscription/components/SubscribeButton.vue', () => ({
default: {
name: 'SubscribeButtonMock',
render() {
return h('div', 'Subscribe Button')
}
}
}))
describe('CurrentUserPopover', () => { describe('CurrentUserPopover', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()
@@ -137,9 +148,9 @@ describe('CurrentUserPopover', () => {
it('renders logout button with correct props', () => { it('renders logout button with correct props', () => {
const wrapper = mountComponent() const wrapper = mountComponent()
// Find all buttons and get the logout button (second one) // Find all buttons and get the logout button (last button)
const buttons = wrapper.findAllComponents(Button) const buttons = wrapper.findAllComponents(Button)
const logoutButton = buttons[1] const logoutButton = buttons[4]
// Check that logout button has correct props // Check that logout button has correct props
expect(logoutButton.props('label')).toBe('Log Out') expect(logoutButton.props('label')).toBe('Log Out')
@@ -149,9 +160,9 @@ describe('CurrentUserPopover', () => {
it('opens user settings and emits close event when settings button is clicked', async () => { it('opens user settings and emits close event when settings button is clicked', async () => {
const wrapper = mountComponent() const wrapper = mountComponent()
// Find all buttons and get the settings button (first one) // Find all buttons and get the settings button (third button)
const buttons = wrapper.findAllComponents(Button) const buttons = wrapper.findAllComponents(Button)
const settingsButton = buttons[0] const settingsButton = buttons[2]
// Click the settings button // Click the settings button
await settingsButton.trigger('click') await settingsButton.trigger('click')
@@ -167,9 +178,9 @@ describe('CurrentUserPopover', () => {
it('calls logout function and emits close event when logout button is clicked', async () => { it('calls logout function and emits close event when logout button is clicked', async () => {
const wrapper = mountComponent() const wrapper = mountComponent()
// Find all buttons and get the logout button (second one) // Find all buttons and get the logout button (last button)
const buttons = wrapper.findAllComponents(Button) const buttons = wrapper.findAllComponents(Button)
const logoutButton = buttons[1] const logoutButton = buttons[4]
// Click the logout button // Click the logout button
await logoutButton.trigger('click') await logoutButton.trigger('click')
@@ -185,16 +196,16 @@ describe('CurrentUserPopover', () => {
it('opens API pricing docs and emits close event when API pricing button is clicked', async () => { it('opens API pricing docs and emits close event when API pricing button is clicked', async () => {
const wrapper = mountComponent() const wrapper = mountComponent()
// Find all buttons and get the API pricing button (third one now) // Find all buttons and get the Partner Nodes info button (first one)
const buttons = wrapper.findAllComponents(Button) const buttons = wrapper.findAllComponents(Button)
const apiPricingButton = buttons[2] const partnerNodesButton = buttons[0]
// Click the API pricing button // Click the Partner Nodes button
await apiPricingButton.trigger('click') await partnerNodesButton.trigger('click')
// Verify window.open was called with the correct URL // Verify window.open was called with the correct URL
expect(window.open).toHaveBeenCalledWith( expect(window.open).toHaveBeenCalledWith(
'https://docs.comfy.org/tutorials/api-nodes/pricing', 'https://docs.comfy.org/tutorials/api-nodes/overview#api-nodes',
'_blank' '_blank'
) )
@@ -206,9 +217,9 @@ describe('CurrentUserPopover', () => {
it('opens top-up dialog and emits close event when top-up button is clicked', async () => { it('opens top-up dialog and emits close event when top-up button is clicked', async () => {
const wrapper = mountComponent() const wrapper = mountComponent()
// Find all buttons and get the top-up button (last one) // Find all buttons and get the top-up button (second one)
const buttons = wrapper.findAllComponents(Button) const buttons = wrapper.findAllComponents(Button)
const topUpButton = buttons[buttons.length - 1] const topUpButton = buttons[1]
// Click the top-up button // Click the top-up button
await topUpButton.trigger('click') await topUpButton.trigger('click')

View File

@@ -23,6 +23,38 @@
</div> </div>
</div> </div>
<div v-if="isActiveSubscription" class="flex items-center justify-between">
<div class="flex flex-col gap-1">
<UserCredit text-class="text-2xl" />
<Button
:label="$t('subscription.partnerNodesCredits')"
severity="secondary"
text
size="small"
class="pl-6 p-0 h-auto justify-start"
:pt="{
root: {
class: 'hover:bg-transparent active:bg-transparent'
}
}"
@click="handleOpenPartnerNodesInfo"
/>
</div>
<Button
:label="$t('credits.topUp.topUp')"
severity="secondary"
size="small"
@click="handleTopUp"
/>
</div>
<SubscribeButton
v-else
:label="$t('subscription.subscribeToComfyCloud')"
size="small"
variant="gradient"
@subscribed="handleSubscribed"
/>
<Divider class="my-2" /> <Divider class="my-2" />
<Button <Button
@@ -35,6 +67,17 @@
@click="handleOpenUserSettings" @click="handleOpenUserSettings"
/> />
<Button
v-if="isActiveSubscription"
class="justify-start"
:label="$t(planSettingsLabel)"
icon="pi pi-receipt"
text
fluid
severity="secondary"
@click="handleOpenPlanAndCreditsSettings"
/>
<Divider class="my-2" /> <Divider class="my-2" />
<Button <Button
@@ -46,34 +89,6 @@
severity="secondary" severity="secondary"
@click="handleLogout" @click="handleLogout"
/> />
<Divider class="my-2" />
<Button
class="justify-start"
:label="$t('credits.apiPricing')"
icon="pi pi-external-link"
text
fluid
severity="secondary"
@click="handleOpenApiPricing"
/>
<Divider class="my-2" />
<div class="flex w-full flex-col gap-2 p-2">
<div class="text-sm text-muted">
{{ $t('credits.yourCreditBalance') }}
</div>
<div class="flex items-center justify-between">
<UserCredit text-class="text-2xl" />
<Button
v-if="isActiveSubscription"
:label="$t('credits.topUp.topUp')"
@click="handleTopUp"
/>
</div>
</div>
</div> </div>
</template> </template>
@@ -86,37 +101,60 @@ import UserAvatar from '@/components/common/UserAvatar.vue'
import UserCredit from '@/components/common/UserCredit.vue' import UserCredit from '@/components/common/UserCredit.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser' import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions' import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
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 { isCloud } from '@/platform/distribution/types'
import { useDialogService } from '@/services/dialogService' import { useDialogService } from '@/services/dialogService'
const emit = defineEmits<{ const emit = defineEmits<{
close: [] close: []
}>() }>()
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 dialogService = useDialogService() const dialogService = useDialogService()
const { isActiveSubscription } = useSubscription() const { isActiveSubscription, fetchStatus } = useSubscription()
const handleOpenUserSettings = () => { const handleOpenUserSettings = () => {
dialogService.showSettingsDialog('user') dialogService.showSettingsDialog('user')
emit('close') emit('close')
} }
const handleOpenPlanAndCreditsSettings = () => {
if (isCloud) {
dialogService.showSettingsDialog('subscription')
} else {
dialogService.showSettingsDialog('credits')
}
emit('close')
}
const handleTopUp = () => { const handleTopUp = () => {
dialogService.showTopUpCreditsDialog() dialogService.showTopUpCreditsDialog()
emit('close') emit('close')
} }
const handleOpenPartnerNodesInfo = () => {
window.open(
'https://docs.comfy.org/tutorials/api-nodes/overview#api-nodes',
'_blank'
)
emit('close')
}
const handleLogout = async () => { const handleLogout = async () => {
await handleSignOut() await handleSignOut()
emit('close') emit('close')
} }
const handleOpenApiPricing = () => { const handleSubscribed = async () => {
window.open('https://docs.comfy.org/tutorials/api-nodes/pricing', '_blank') await fetchStatus()
emit('close')
} }
onMounted(() => { onMounted(() => {

View File

@@ -1687,7 +1687,9 @@
"subscribe": "Subscribe" "subscribe": "Subscribe"
}, },
"subscribeToRun": "Subscribe to Run", "subscribeToRun": "Subscribe to Run",
"subscribeNow": "Subscribe Now" "subscribeNow": "Subscribe Now",
"subscribeToComfyCloud": "Subscribe to Comfy Cloud",
"partnerNodesCredits": "Partner Nodes credits"
}, },
"userSettings": { "userSettings": {
"title": "User Settings", "title": "User Settings",

View File

@@ -5,9 +5,17 @@
:loading="isLoading" :loading="isLoading"
:disabled="isPolling" :disabled="isPolling"
severity="primary" severity="primary"
:style="
variant === 'gradient'
? {
background: 'var(--color-subscription-button-gradient)',
color: 'var(--color-white)'
}
: undefined
"
:pt="{ :pt="{
root: { root: {
class: 'w-full font-bold' class: rootClass
} }
}" }"
@click="handleSubscribe" @click="handleSubscribe"
@@ -16,22 +24,29 @@
<script setup lang="ts"> <script setup lang="ts">
import Button from 'primevue/button' import Button from 'primevue/button'
import { onBeforeUnmount, ref } from 'vue' import { computed, onBeforeUnmount, ref } from 'vue'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription' import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { isCloud } from '@/platform/distribution/types' import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry' import { useTelemetry } from '@/platform/telemetry'
import { cn } from '@/utils/tailwindUtil'
withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
label?: string label?: string
size?: 'small' | 'large' size?: 'small' | 'large'
variant?: 'default' | 'gradient'
fluid?: boolean
}>(), }>(),
{ {
size: 'large' size: 'large',
variant: 'default',
fluid: true
} }
) )
const rootClass = computed(() => cn('font-bold', props.fluid && 'w-full'))
const emit = defineEmits<{ const emit = defineEmits<{
subscribed: [] subscribed: []
}>() }>()

View File

@@ -9,6 +9,10 @@
icon="pi pi-lock" icon="pi pi-lock"
severity="primary" severity="primary"
size="small" size="small"
:style="{
background: 'var(--color-subscription-button-gradient)',
color: 'var(--color-white)'
}"
data-testid="subscribe-to-run-button" data-testid="subscribe-to-run-button"
@click="handleSubscribeToRun" @click="handleSubscribeToRun"
/> />

View File

@@ -61,6 +61,7 @@
v-else v-else
:label="$t('subscription.subscribeNow')" :label="$t('subscription.subscribeNow')"
size="small" size="small"
:fluid="false"
class="text-xs" class="text-xs"
@subscribed="handleRefresh" @subscribed="handleRefresh"
/> />