[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" />
</div>
<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>
<i
:class="
@@ -18,7 +23,9 @@
/>
</template>
</Tag>
<div :class="textClass">{{ formattedBalance }}</div>
<div :class="textClass">
{{ showCreditsOnly ? formattedCreditsOnly : formattedBalance }}
</div>
</div>
</template>
@@ -32,8 +39,9 @@ import { formatCreditsFromCents } from '@/base/credits/comfyCredits'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
const { textClass } = defineProps<{
const { textClass, showCreditsOnly } = defineProps<{
textClass?: string
showCreditsOnly?: boolean
}>()
const authStore = useFirebaseAuthStore()
@@ -50,4 +58,14 @@ const formattedBalance = computed(() => {
})
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>

View File

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

View File

@@ -1836,6 +1836,8 @@
"seeDetails": "See details",
"topUp": "Top Up",
"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.",
"howManyCredits": "How many credits would you like to add?",
"videosEstimate": "~{count} videos*",
@@ -1858,7 +1860,7 @@
"accountInitialized": "Account initialized",
"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": {

View File

@@ -386,11 +386,12 @@ export const useDialogService = () => {
return dialogStore.showDialog({
key: 'top-up-credits',
component: TopUpCreditsDialogContent,
headerComponent: ComfyOrgHeader,
props: options,
dialogComponentProps: {
headless: true,
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*')
})
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', () => {
const wrapper = mountOption({ selected: false })
expect(wrapper.find('div').classes()).toContain('bg-surface-tertiary')
expect(wrapper.find('div').classes()).toContain('border-border-primary')
expect(wrapper.find('div').classes()).toContain(
'bg-component-node-disabled'
)
expect(wrapper.find('div').classes()).toContain('border-transparent')
})
it('emits select event when clicked', async () => {