[backport cloud/1.34] style: redesign TopUpCredits dialog (#7313)

Backport of #7305 to `cloud/1.34`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7313-backport-cloud-1-34-style-redesign-TopUpCredits-dialog-2c56d73d36508172b633f5602a8b967d)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
This commit is contained in:
Comfy Org PR Bot
2025-12-10 14:30:05 +09:00
committed by GitHub
parent c82f4272a7
commit ab74061bc6
6 changed files with 79 additions and 51 deletions

View File

@@ -7,7 +7,12 @@
<Skeleton width="8rem" height="2rem" /> <Skeleton width="8rem" height="2rem" />
</div> </div>
<div v-else class="flex items-center gap-1"> <div v-else class="flex items-center gap-1">
<Tag severity="secondary" rounded class="p-1 text-amber-400"> <Tag
v-if="!showCreditsOnly"
severity="secondary"
rounded
class="p-1 text-amber-400"
>
<template #icon> <template #icon>
<i <i
:class=" :class="
@@ -18,7 +23,9 @@
/> />
</template> </template>
</Tag> </Tag>
<div :class="textClass">{{ formattedBalance }}</div> <div :class="textClass">
{{ showCreditsOnly ? formattedCreditsOnly : formattedBalance }}
</div>
</div> </div>
</template> </template>
@@ -32,8 +39,9 @@ import { formatCreditsFromCents } from '@/base/credits/comfyCredits'
import { useFeatureFlags } from '@/composables/useFeatureFlags' import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
const { textClass } = defineProps<{ const { textClass, showCreditsOnly } = defineProps<{
textClass?: string textClass?: string
showCreditsOnly?: boolean
}>() }>()
const authStore = useFirebaseAuthStore() const authStore = useFirebaseAuthStore()
@@ -50,4 +58,14 @@ const formattedBalance = computed(() => {
}) })
return `${amount} ${t('credits.credits')}` return `${amount} ${t('credits.credits')}`
}) })
const formattedCreditsOnly = computed(() => {
// Backend returns cents despite the *_micros naming convention.
const cents = authStore.balance?.amount_micros ?? 0
const amount = formatCreditsFromCents({
cents,
locale: locale.value
})
return amount
})
</script> </script>

View File

@@ -1,35 +1,43 @@
<template> <template>
<!-- New Credits Design (default) --> <!-- New Credits Design (default) -->
<div <div v-if="useNewDesign" class="flex w-112 flex-col gap-8 p-8">
v-if="useNewDesign"
class="flex w-96 flex-col gap-8 p-8 bg-node-component-surface rounded-2xl border border-border-primary"
>
<!-- Header --> <!-- Header -->
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<h1 class="text-2xl font-semibold text-foreground-primary m-0"> <h1 class="text-2xl font-semibold text-white m-0">
{{ $t('credits.topUp.addMoreCredits') }} {{
isInsufficientCredits
? $t('credits.topUp.addMoreCreditsToRun')
: $t('credits.topUp.addMoreCredits')
}}
</h1> </h1>
<p class="text-sm text-foreground-secondary m-0"> <div v-if="isInsufficientCredits" class="flex flex-col gap-2">
{{ $t('credits.topUp.creditsDescription') }} <p class="text-sm text-muted-foreground m-0 w-96">
</p> {{ $t('credits.topUp.insufficientWorkflowMessage') }}
</p>
</div>
<div v-else class="flex flex-col gap-2">
<p class="text-sm text-muted-foreground m-0">
{{ $t('credits.topUp.creditsDescription') }}
</p>
</div>
</div> </div>
<!-- Current Balance Section --> <!-- Current Balance Section -->
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="flex items-baseline gap-2"> <div class="flex items-baseline gap-2">
<UserCredit text-class="text-3xl font-bold" /> <UserCredit text-class="text-3xl font-bold" show-credits-only />
<span class="text-sm text-foreground-secondary">{{ <span class="text-sm text-muted-foreground">{{
$t('credits.creditsAvailable') $t('credits.creditsAvailable')
}}</span> }}</span>
</div> </div>
<div v-if="refreshDate" class="text-sm text-foreground-secondary"> <div v-if="formattedRenewalDate" class="text-sm text-muted-foreground">
{{ $t('credits.refreshes', { date: refreshDate }) }} {{ $t('credits.refreshes', { date: formattedRenewalDate }) }}
</div> </div>
</div> </div>
<!-- Credit Options Section --> <!-- Credit Options Section -->
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<span class="text-sm text-foreground-secondary"> <span class="text-sm text-muted-foreground">
{{ $t('credits.topUp.howManyCredits') }} {{ $t('credits.topUp.howManyCredits') }}
</span> </span>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@@ -42,7 +50,7 @@
@select="selectedCredits = option.credits" @select="selectedCredits = option.credits"
/> />
</div> </div>
<div class="text-xs text-foreground-secondary"> <div class="text-xs text-muted-foreground w-96">
{{ $t('credits.topUp.templateNote') }} {{ $t('credits.topUp.templateNote') }}
</div> </div>
</div> </div>
@@ -53,7 +61,8 @@
:loading="loading" :loading="loading"
severity="primary" severity="primary"
:label="$t('credits.topUp.buy')" :label="$t('credits.topUp.buy')"
class="w-full" :class="['w-full', { 'opacity-30': !selectedCredits || loading }]"
:pt="{ label: { class: 'text-white' } }"
@click="handleBuy" @click="handleBuy"
/> />
</div> </div>
@@ -121,6 +130,7 @@ import {
import UserCredit from '@/components/common/UserCredit.vue' import UserCredit from '@/components/common/UserCredit.vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions' import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useFeatureFlags } from '@/composables/useFeatureFlags' import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { useTelemetry } from '@/platform/telemetry' import { useTelemetry } from '@/platform/telemetry'
import CreditTopUpOption from './credit/CreditTopUpOption.vue' import CreditTopUpOption from './credit/CreditTopUpOption.vue'
@@ -132,18 +142,17 @@ interface CreditOption {
} }
const { const {
refreshDate,
isInsufficientCredits = false, isInsufficientCredits = false,
amountOptions = [5, 10, 20, 50], amountOptions = [5, 10, 20, 50],
preselectedAmountOption = 10 preselectedAmountOption = 10
} = defineProps<{ } = defineProps<{
refreshDate?: string
isInsufficientCredits?: boolean isInsufficientCredits?: boolean
amountOptions?: number[] amountOptions?: number[]
preselectedAmountOption?: number preselectedAmountOption?: number
}>() }>()
const { flags } = useFeatureFlags() const { flags } = useFeatureFlags()
const { formattedRenewalDate } = useSubscription()
// Use feature flag to determine design - defaults to true (new design) // Use feature flag to determine design - defaults to true (new design)
const useNewDesign = computed(() => flags.subscriptionTiersEnabled) const useNewDesign = computed(() => flags.subscriptionTiersEnabled)
@@ -157,20 +166,20 @@ const loading = ref(false)
const creditOptions: CreditOption[] = [ const creditOptions: CreditOption[] = [
{ {
credits: 1000, credits: 1055, // $5.00
description: t('credits.topUp.videosEstimate', { count: 100 }) description: t('credits.topUp.videosEstimate', { count: 41 })
}, },
{ {
credits: 5000, credits: 2110, // $10.00
description: t('credits.topUp.videosEstimate', { count: 500 }) description: t('credits.topUp.videosEstimate', { count: 82 })
}, },
{ {
credits: 10000, credits: 4220, // $20.00
description: t('credits.topUp.videosEstimate', { count: 1000 }) description: t('credits.topUp.videosEstimate', { count: 184 })
}, },
{ {
credits: 20000, credits: 10550, // $50.00
description: t('credits.topUp.videosEstimate', { count: 2000 }) description: t('credits.topUp.videosEstimate', { count: 412 })
} }
] ]

View File

@@ -3,19 +3,17 @@
class="flex items-center justify-between p-2 rounded-lg cursor-pointer transition-all duration-200" class="flex items-center justify-between p-2 rounded-lg cursor-pointer transition-all duration-200"
:class="[ :class="[
selected selected
? 'bg-surface-secondary border-2 border-primary' ? 'bg-secondary-background border-2 border-border-default'
: 'bg-surface-tertiary border border-border-primary hover:bg-surface-secondary' : 'bg-component-node-disabled hover:bg-secondary-background border-2 border-transparent'
]" ]"
@click="$emit('select')" @click="$emit('select')"
> >
<div class="flex flex-col"> <span class="text-base font-bold text-white">
<span class="text-base font-medium text-foreground-primary"> {{ formattedCredits }}
{{ formattedCredits }} </span>
</span> <span class="text-sm font-normal text-white">
<span class="text-sm text-foreground-secondary"> {{ description }}
{{ description }} </span>
</span>
</div>
</div> </div>
</template> </template>
@@ -38,6 +36,10 @@ defineEmits<{
const { locale } = useI18n() const { locale } = useI18n()
const formattedCredits = computed(() => { const formattedCredits = computed(() => {
return formatCredits({ value: credits, locale: locale.value }) return formatCredits({
value: credits,
locale: locale.value,
numberOptions: { minimumFractionDigits: 0, maximumFractionDigits: 0 }
})
}) })
</script> </script>

View File

@@ -1836,6 +1836,8 @@
"seeDetails": "See details", "seeDetails": "See details",
"topUp": "Top Up", "topUp": "Top Up",
"addMoreCredits": "Add more credits", "addMoreCredits": "Add more credits",
"addMoreCreditsToRun": "Add more credits to run",
"insufficientWorkflowMessage": "You don't have enough credits to run this workflow.",
"creditsDescription": "Credits are used to run workflows or partner nodes.", "creditsDescription": "Credits are used to run workflows or partner nodes.",
"howManyCredits": "How many credits would you like to add?", "howManyCredits": "How many credits would you like to add?",
"videosEstimate": "~{count} videos*", "videosEstimate": "~{count} videos*",
@@ -1858,7 +1860,7 @@
"accountInitialized": "Account initialized", "accountInitialized": "Account initialized",
"unified": { "unified": {
"message": "Credits have been unified", "message": "Credits have been unified",
"tooltip": "We've unified payments across Comfy. Everything now runs on Comfy Credits:\n- Partner Nodes (formerly API nodes)\n- Cloud workflows\n\nYour existing Partner node balance has been converted into credits.\nLearn more here." "tooltip": "We've unified payments across Comfy. Everything now runs on Comfy Credits:\n- Partner Nodes (formerly API nodes)\n- Cloud workflows\n\nYour existing Partner node balance has been converted into credits."
} }
}, },
"subscription": { "subscription": {

View File

@@ -386,11 +386,12 @@ export const useDialogService = () => {
return dialogStore.showDialog({ return dialogStore.showDialog({
key: 'top-up-credits', key: 'top-up-credits',
component: TopUpCreditsDialogContent, component: TopUpCreditsDialogContent,
headerComponent: ComfyOrgHeader,
props: options, props: options,
dialogComponentProps: { dialogComponentProps: {
headless: true,
pt: { pt: {
header: { class: 'p-3!' } header: { class: 'p-0! hidden' },
content: { class: 'p-0! m-0!' }
} }
} }
}) })

View File

@@ -32,16 +32,12 @@ describe('CreditTopUpOption', () => {
expect(wrapper.text()).toContain('~500 videos*') expect(wrapper.text()).toContain('~500 videos*')
}) })
it('applies selected styling when selected', () => {
const wrapper = mountOption({ selected: true })
expect(wrapper.find('div').classes()).toContain('bg-surface-secondary')
expect(wrapper.find('div').classes()).toContain('border-primary')
})
it('applies unselected styling when not selected', () => { it('applies unselected styling when not selected', () => {
const wrapper = mountOption({ selected: false }) const wrapper = mountOption({ selected: false })
expect(wrapper.find('div').classes()).toContain('bg-surface-tertiary') expect(wrapper.find('div').classes()).toContain(
expect(wrapper.find('div').classes()).toContain('border-border-primary') 'bg-component-node-disabled'
)
expect(wrapper.find('div').classes()).toContain('border-transparent')
}) })
it('emits select event when clicked', async () => { it('emits select event when clicked', async () => {