mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-26 17:30:07 +00:00
feat: update subscription panel with tier-based design and improved UX (#7307)
Transforms the subscription credits panel from legacy design to tier-based layout with Creator tier details, updated typography using design system tokens, improved responsive credit breakdown layout, and better subscription management flow. Updates credit formatting to remove unnecessary decimals and Credits suffix, replaces external Stripe billing portal with inline dialog, and reorganizes plan benefits section with proper v-for structure matching Figma specifications. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7307-feat-update-subscription-panel-with-tier-based-design-and-improved-UX-2c56d73d365081ef8b63e262a6822c72) by [Unito](https://www.unito.io) --------- Co-authored-by: Alexander Brown <drjkl@comfy.org>
This commit is contained in:
@@ -1,42 +1,19 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-start gap-1 self-stretch">
|
||||
<div class="flex items-start gap-2">
|
||||
<i class="pi pi-check mt-1 text-xs text-text-primary" />
|
||||
<div class="flex flex-col items-start gap-0 self-stretch">
|
||||
<div class="flex items-center gap-2 py-2">
|
||||
<i class="pi pi-check text-xs text-text-primary" />
|
||||
<span class="text-sm text-text-primary">
|
||||
{{ $t('subscription.benefits.benefit1') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-2">
|
||||
<i class="pi pi-check mt-1 text-xs text-text-primary" />
|
||||
<div class="flex items-center gap-2 py-2">
|
||||
<i class="pi pi-check text-xs text-text-primary" />
|
||||
<span class="text-sm text-text-primary">
|
||||
{{ $t('subscription.benefits.benefit2') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
:label="$t('subscription.viewMoreDetails')"
|
||||
text
|
||||
icon="pi pi-external-link"
|
||||
icon-pos="left"
|
||||
class="flex h-8 min-h-6 py-2 px-0 items-center gap-2 rounded text-text-secondary"
|
||||
:pt="{
|
||||
icon: {
|
||||
class: 'text-xs text-text-secondary'
|
||||
},
|
||||
label: {
|
||||
class: 'text-sm text-text-secondary'
|
||||
}
|
||||
}"
|
||||
@click="handleViewMoreDetails"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
const handleViewMoreDetails = () => {
|
||||
window.open('https://www.comfy.org/cloud/pricing', '_blank')
|
||||
}
|
||||
</script>
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
@@ -19,9 +19,12 @@
|
||||
<div class="rounded-2xl border border-interface-stroke p-6">
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-sm font-bold text-text-primary">
|
||||
{{ tierName }}
|
||||
</div>
|
||||
<div class="flex items-baseline gap-1 font-inter font-semibold">
|
||||
<span class="text-2xl">{{ formattedMonthlyPrice }}</span>
|
||||
<span class="text-2xl">${{ tierPrice }}</span>
|
||||
<span class="text-base">{{
|
||||
$t('subscription.perMonth')
|
||||
}}</span>
|
||||
@@ -59,7 +62,7 @@
|
||||
class: 'text-text-primary'
|
||||
}
|
||||
}"
|
||||
@click="manageSubscription"
|
||||
@click="showSubscriptionDialog"
|
||||
/>
|
||||
<SubscribeButton
|
||||
v-else
|
||||
@@ -75,17 +78,6 @@
|
||||
<div class="grid grid-cols-1 gap-6 pt-9 lg:grid-cols-2">
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col">
|
||||
<div class="text-sm">
|
||||
{{ $t('subscription.partnerNodesBalance') }}
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="text-sm text-muted">
|
||||
{{ $t('subscription.partnerNodesDescription') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
@@ -112,7 +104,7 @@
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-sm text-text-secondary">
|
||||
<div class="text-sm text-muted">
|
||||
{{ $t('subscription.totalCredits') }}
|
||||
</div>
|
||||
<Skeleton
|
||||
@@ -133,12 +125,18 @@
|
||||
width="3rem"
|
||||
height="1rem"
|
||||
/>
|
||||
<div v-else class="text-sm text-text-secondary font-bold">
|
||||
<div
|
||||
v-else
|
||||
class="text-sm font-bold w-12 shrink-0 text-left text-muted"
|
||||
>
|
||||
{{ monthlyBonusCredits }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="text-sm text-text-secondary">
|
||||
{{ $t('subscription.monthlyBonusDescription') }}
|
||||
<div class="flex items-center gap-1 min-w-0">
|
||||
<div
|
||||
class="text-sm truncate text-muted"
|
||||
:title="$t('subscription.creditsRemainingThisMonth')"
|
||||
>
|
||||
{{ $t('subscription.creditsRemainingThisMonth') }}
|
||||
</div>
|
||||
<Button
|
||||
v-tooltip="refreshTooltip"
|
||||
@@ -146,7 +144,7 @@
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
class="h-4 w-4"
|
||||
class="h-4 w-4 shrink-0"
|
||||
:pt="{
|
||||
icon: {
|
||||
class: 'text-text-secondary text-xs'
|
||||
@@ -161,12 +159,18 @@
|
||||
width="3rem"
|
||||
height="1rem"
|
||||
/>
|
||||
<div v-else class="text-sm text-text-secondary font-bold">
|
||||
<div
|
||||
v-else
|
||||
class="text-sm font-bold w-12 shrink-0 text-left text-muted"
|
||||
>
|
||||
{{ prepaidCredits }}
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="text-sm text-text-secondary">
|
||||
{{ $t('subscription.prepaidDescription') }}
|
||||
<div class="flex items-center gap-1 min-w-0">
|
||||
<div
|
||||
class="text-sm truncate text-muted"
|
||||
:title="$t('subscription.creditsYouveAdded')"
|
||||
>
|
||||
{{ $t('subscription.creditsYouveAdded') }}
|
||||
</div>
|
||||
<Button
|
||||
v-tooltip="$t('subscription.prepaidCreditsInfo')"
|
||||
@@ -174,7 +178,7 @@
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
class="h-4 w-4"
|
||||
class="h-4 w-4 shrink-0"
|
||||
:pt="{
|
||||
icon: {
|
||||
class: 'text-text-secondary text-xs'
|
||||
@@ -190,8 +194,7 @@
|
||||
href="https://platform.comfy.org/profile/usage"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm text-text-secondary underline hover:text-text-secondary"
|
||||
style="text-decoration: underline"
|
||||
class="text-sm underline text-center text-muted"
|
||||
>
|
||||
{{ $t('subscription.viewUsageHistory') }}
|
||||
</a>
|
||||
@@ -216,14 +219,47 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 flex-1">
|
||||
<div class="text-sm">
|
||||
<div class="text-sm text-text-primary">
|
||||
{{ $t('subscription.yourPlanIncludes') }}
|
||||
</div>
|
||||
|
||||
<SubscriptionBenefits />
|
||||
<div class="flex flex-col gap-0">
|
||||
<div
|
||||
v-for="benefit in tierBenefits"
|
||||
:key="benefit.key"
|
||||
class="flex items-center gap-2 py-2"
|
||||
>
|
||||
<i
|
||||
v-if="benefit.type === 'feature'"
|
||||
class="pi pi-check text-xs text-text-primary"
|
||||
/>
|
||||
<span
|
||||
v-else-if="benefit.type === 'metric' && benefit.value"
|
||||
class="text-sm font-normal whitespace-nowrap text-text-primary"
|
||||
>
|
||||
{{ benefit.value }}
|
||||
</span>
|
||||
<span class="text-sm text-muted">
|
||||
{{ benefit.label }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- View More Details - Outside main content -->
|
||||
<div class="flex items-center gap-2 py-4">
|
||||
<i class="pi pi-external-link text-muted"></i>
|
||||
<a
|
||||
href="https://www.comfy.org/cloud/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-sm underline hover:opacity-80 text-muted"
|
||||
>
|
||||
{{ $t('subscription.viewMoreDetailsPlans') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -307,28 +343,79 @@
|
||||
import Button from 'primevue/button'
|
||||
import Skeleton from 'primevue/skeleton'
|
||||
import TabPanel from 'primevue/tabpanel'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import CloudBadge from '@/components/topbar/CloudBadge.vue'
|
||||
import { useExternalLink } from '@/composables/useExternalLink'
|
||||
import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue'
|
||||
import SubscriptionBenefits from '@/platform/cloud/subscription/components/SubscriptionBenefits.vue'
|
||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||
import { useSubscriptionActions } from '@/platform/cloud/subscription/composables/useSubscriptionActions'
|
||||
import { useSubscriptionCredits } from '@/platform/cloud/subscription/composables/useSubscriptionCredits'
|
||||
import { useSubscriptionDialog } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const { buildDocsUrl } = useExternalLink()
|
||||
const { t } = useI18n()
|
||||
|
||||
const {
|
||||
isActiveSubscription,
|
||||
isCancelled,
|
||||
formattedRenewalDate,
|
||||
formattedEndDate,
|
||||
formattedMonthlyPrice,
|
||||
manageSubscription,
|
||||
handleInvoiceHistory
|
||||
} = useSubscription()
|
||||
|
||||
const { show: showSubscriptionDialog } = useSubscriptionDialog()
|
||||
|
||||
// Tier data - hardcoded for Creator tier as requested
|
||||
const tierName = computed(() => t('subscription.tiers.creator.name'))
|
||||
const tierPrice = computed(() => t('subscription.tiers.creator.price'))
|
||||
|
||||
// Tier benefits for v-for loop
|
||||
type BenefitType = 'metric' | 'feature'
|
||||
|
||||
interface Benefit {
|
||||
key: string
|
||||
type: BenefitType
|
||||
label: string
|
||||
value?: string
|
||||
}
|
||||
|
||||
const tierBenefits = computed(() => {
|
||||
const baseBenefits: Benefit[] = [
|
||||
{
|
||||
key: 'monthlyCredits',
|
||||
type: 'metric',
|
||||
value: t('subscription.tiers.creator.benefits.monthlyCredits'),
|
||||
label: t('subscription.tiers.creator.benefits.monthlyCreditsLabel')
|
||||
},
|
||||
{
|
||||
key: 'maxDuration',
|
||||
type: 'metric',
|
||||
value: t('subscription.tiers.creator.benefits.maxDuration'),
|
||||
label: t('subscription.tiers.creator.benefits.maxDurationLabel')
|
||||
},
|
||||
{
|
||||
key: 'gpu',
|
||||
type: 'feature',
|
||||
label: t('subscription.tiers.creator.benefits.gpuLabel')
|
||||
},
|
||||
{
|
||||
key: 'addCredits',
|
||||
type: 'feature',
|
||||
label: t('subscription.tiers.creator.benefits.addCreditsLabel')
|
||||
},
|
||||
{
|
||||
key: 'customLoRAs',
|
||||
type: 'feature',
|
||||
label: t('subscription.tiers.creator.benefits.customLoRAsLabel')
|
||||
}
|
||||
]
|
||||
|
||||
return baseBenefits
|
||||
})
|
||||
|
||||
const { totalCredits, monthlyBonusCredits, prepaidCredits, isLoadingBalance } =
|
||||
useSubscriptionCredits()
|
||||
|
||||
|
||||
@@ -9,16 +9,20 @@ import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||
*/
|
||||
export function useSubscriptionCredits() {
|
||||
const authStore = useFirebaseAuthStore()
|
||||
const { t, locale } = useI18n()
|
||||
const { locale } = useI18n()
|
||||
|
||||
const formatBalance = (maybeCents?: number) => {
|
||||
// Backend returns cents despite the *_micros naming convention.
|
||||
const cents = maybeCents ?? 0
|
||||
const amount = formatCreditsFromCents({
|
||||
cents,
|
||||
locale: locale.value
|
||||
locale: locale.value,
|
||||
numberOptions: {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
}
|
||||
})
|
||||
return `${amount} ${t('credits.credits')}`
|
||||
return amount
|
||||
}
|
||||
|
||||
const totalCredits = computed(() =>
|
||||
|
||||
Reference in New Issue
Block a user