mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
[backport cloud/1.35] feat(cloud): yearly pricing (#7581)
Backport of #7572 to `cloud/1.35` Automatically created by backport workflow. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7581-backport-cloud-1-35-feat-cloud-yearly-pricing-2cc6d73d3650814296f1c41377746400) by [Unito](https://www.unito.io) Co-authored-by: Simula_r <18093452+simula-r@users.noreply.github.com>
This commit is contained in:
@@ -235,7 +235,7 @@
|
|||||||
--brand-yellow: var(--color-electric-400);
|
--brand-yellow: var(--color-electric-400);
|
||||||
--brand-blue: var(--color-sapphire-700);
|
--brand-blue: var(--color-sapphire-700);
|
||||||
--secondary-background: var(--color-smoke-200);
|
--secondary-background: var(--color-smoke-200);
|
||||||
--secondary-background-hover: var(--color-smoke-200);
|
--secondary-background-hover: var(--color-smoke-400);
|
||||||
--secondary-background-selected: var(--color-smoke-600);
|
--secondary-background-selected: var(--color-smoke-600);
|
||||||
--base-background: var(--color-white);
|
--base-background: var(--color-white);
|
||||||
--primary-background: var(--color-azure-400);
|
--primary-background: var(--color-azure-400);
|
||||||
|
|||||||
@@ -1895,7 +1895,7 @@
|
|||||||
"comfyCloudLogo": "Comfy Cloud Logo",
|
"comfyCloudLogo": "Comfy Cloud Logo",
|
||||||
"beta": "BETA",
|
"beta": "BETA",
|
||||||
"perMonth": "/ month",
|
"perMonth": "/ month",
|
||||||
"usdPerMonth": "USD / month",
|
"usdPerMonth": "USD / mo",
|
||||||
"renewsDate": "Renews {date}",
|
"renewsDate": "Renews {date}",
|
||||||
"expiresDate": "Expires {date}",
|
"expiresDate": "Expires {date}",
|
||||||
"manageSubscription": "Manage subscription",
|
"manageSubscription": "Manage subscription",
|
||||||
@@ -1917,16 +1917,21 @@
|
|||||||
"yourPlanIncludes": "Your plan includes:",
|
"yourPlanIncludes": "Your plan includes:",
|
||||||
"viewMoreDetails": "View more details",
|
"viewMoreDetails": "View more details",
|
||||||
"learnMore": "Learn more",
|
"learnMore": "Learn more",
|
||||||
|
"billedMonthly": "Billed monthly",
|
||||||
|
"billedAnnually": "{total} Billed annually",
|
||||||
|
"monthly": "Monthly",
|
||||||
|
"yearly": "Yearly",
|
||||||
"messageSupport": "Message support",
|
"messageSupport": "Message support",
|
||||||
"invoiceHistory": "Invoice history",
|
"invoiceHistory": "Invoice history",
|
||||||
"benefits": {
|
"benefits": {
|
||||||
"benefit1": "$10 in monthly credits for Partner Nodes — top up when needed",
|
"benefit1": "$10 in monthly credits for Partner Nodes — top up when needed",
|
||||||
"benefit2": "Up to 30 min runtime per job"
|
"benefit2": "Up to 30 min runtime per job"
|
||||||
},
|
},
|
||||||
|
"yearlyDiscount": "20% DISCOUNT",
|
||||||
"tiers": {
|
"tiers": {
|
||||||
"founder": {
|
"founder": {
|
||||||
"name": "Founder's Edition",
|
"name": "Founder's Edition",
|
||||||
"price": "20.00",
|
"price": "20",
|
||||||
"benefits": {
|
"benefits": {
|
||||||
"monthlyCredits": "5,460",
|
"monthlyCredits": "5,460",
|
||||||
"monthlyCreditsLabel": "monthly credits",
|
"monthlyCreditsLabel": "monthly credits",
|
||||||
@@ -1939,7 +1944,11 @@
|
|||||||
},
|
},
|
||||||
"standard": {
|
"standard": {
|
||||||
"name": "Standard",
|
"name": "Standard",
|
||||||
"price": "20.00",
|
"price": {
|
||||||
|
"monthly": "20",
|
||||||
|
"yearly": "16",
|
||||||
|
"annualTotal": "$192"
|
||||||
|
},
|
||||||
"benefits": {
|
"benefits": {
|
||||||
"monthlyCredits": "4,200",
|
"monthlyCredits": "4,200",
|
||||||
"monthlyCreditsLabel": "monthly credits",
|
"monthlyCreditsLabel": "monthly credits",
|
||||||
@@ -1953,7 +1962,12 @@
|
|||||||
},
|
},
|
||||||
"creator": {
|
"creator": {
|
||||||
"name": "Creator",
|
"name": "Creator",
|
||||||
"price": "35.00",
|
"price": {
|
||||||
|
"monthly": "35",
|
||||||
|
"yearly": "28",
|
||||||
|
"annualTotal": "$336"
|
||||||
|
},
|
||||||
|
|
||||||
"benefits": {
|
"benefits": {
|
||||||
"monthlyCredits": "7,400",
|
"monthlyCredits": "7,400",
|
||||||
"monthlyCreditsLabel": "monthly credits",
|
"monthlyCreditsLabel": "monthly credits",
|
||||||
@@ -1967,7 +1981,11 @@
|
|||||||
},
|
},
|
||||||
"pro": {
|
"pro": {
|
||||||
"name": "Pro",
|
"name": "Pro",
|
||||||
"price": "100.00",
|
"price": {
|
||||||
|
"monthly": "100",
|
||||||
|
"yearly": "80",
|
||||||
|
"annualTotal": "$960"
|
||||||
|
},
|
||||||
"benefits": {
|
"benefits": {
|
||||||
"monthlyCredits": "21,100",
|
"monthlyCredits": "21,100",
|
||||||
"monthlyCreditsLabel": "monthly credits",
|
"monthlyCreditsLabel": "monthly credits",
|
||||||
@@ -1998,7 +2016,7 @@
|
|||||||
"description": "Choose the best plan for you",
|
"description": "Choose the best plan for you",
|
||||||
"haveQuestions": "Have questions or wondering about enterprise?",
|
"haveQuestions": "Have questions or wondering about enterprise?",
|
||||||
"contactUs": "Contact us",
|
"contactUs": "Contact us",
|
||||||
"viewEnterprise": "view enterprise",
|
"viewEnterprise": "View enterprise",
|
||||||
"partnerNodesCredits": "Partner nodes pricing",
|
"partnerNodesCredits": "Partner nodes pricing",
|
||||||
"mostPopular": "Most popular",
|
"mostPopular": "Most popular",
|
||||||
"currentPlan": "Current Plan",
|
"currentPlan": "Current Plan",
|
||||||
|
|||||||
@@ -1,171 +1,246 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-row items-stretch gap-6">
|
<div class="flex flex-col gap-8">
|
||||||
<div
|
<div class="flex justify-center">
|
||||||
v-for="tier in tiers"
|
<SelectButton
|
||||||
:key="tier.id"
|
v-model="currentBillingCycle"
|
||||||
class="flex-1 flex flex-col rounded-2xl border border-interface-stroke bg-interface-panel-surface shadow-[0_0_12px_rgba(0,0,0,0.1)]"
|
:options="billingCycleOptions"
|
||||||
>
|
option-label="label"
|
||||||
<div class="flex flex-col gap-6 p-8">
|
option-value="value"
|
||||||
<div class="flex flex-row items-center gap-2">
|
:allow-empty="false"
|
||||||
<span
|
unstyled
|
||||||
class="font-inter text-base font-bold leading-normal text-base-foreground"
|
:pt="{
|
||||||
>
|
root: {
|
||||||
{{ tier.name }}
|
class: 'flex gap-1 bg-secondary-background rounded-lg p-1.5'
|
||||||
</span>
|
},
|
||||||
<div
|
pcToggleButton: {
|
||||||
v-if="tier.isPopular"
|
root: ({ context }: ToggleButtonPassThroughMethodOptions) => ({
|
||||||
class="rounded-full bg-background px-1 text-xs font-semibold uppercase tracking-wide text-foreground h-[13px] leading-[13px]"
|
class: [
|
||||||
>
|
'w-36 h-8 rounded-md transition-colors cursor-pointer border-none outline-none ring-0 text-sm font-medium flex items-center justify-center',
|
||||||
{{ t('subscription.mostPopular') }}
|
context.active
|
||||||
</div>
|
? 'bg-base-foreground text-base-background'
|
||||||
</div>
|
: 'bg-transparent text-muted-foreground hover:bg-secondary-background-hover'
|
||||||
<div class="flex flex-row items-baseline gap-2">
|
]
|
||||||
<span
|
}),
|
||||||
class="font-inter text-[32px] font-semibold leading-normal text-base-foreground"
|
label: { class: 'flex items-center gap-2 ' }
|
||||||
>
|
}
|
||||||
${{ tier.price }}
|
}"
|
||||||
</span>
|
>
|
||||||
<span
|
<template #option="{ option }">
|
||||||
class="font-inter text-base font-normal leading-normal text-base-foreground"
|
<div class="flex items-center gap-2">
|
||||||
>
|
<span>{{ option.label }}</span>
|
||||||
{{ t('subscription.usdPerMonth') }}
|
<div
|
||||||
</span>
|
v-if="option.value === 'yearly'"
|
||||||
</div>
|
class="bg-primary-background text-white text-[11px] px-1 py-0.5 rounded-full flex items-center font-bold"
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-4 px-8 pb-0 flex-1">
|
|
||||||
<div class="flex flex-row items-center justify-between">
|
|
||||||
<span
|
|
||||||
class="font-inter text-sm font-normal leading-normal text-muted-foreground"
|
|
||||||
>
|
|
||||||
{{ t('subscription.monthlyCreditsLabel') }}
|
|
||||||
</span>
|
|
||||||
<div class="flex flex-row items-center gap-1">
|
|
||||||
<i class="icon-[lucide--component] text-amber-400 text-sm" />
|
|
||||||
<span
|
|
||||||
class="font-inter text-sm font-bold leading-normal text-base-foreground"
|
|
||||||
>
|
>
|
||||||
{{ tier.credits }}
|
-20%
|
||||||
</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
|
</SelectButton>
|
||||||
<div class="flex flex-row items-center justify-between">
|
</div>
|
||||||
<span class="text-sm font-normal text-muted-foreground">
|
<div class="flex flex-col xl:flex-row items-stretch gap-6">
|
||||||
{{ t('subscription.maxDurationLabel') }}
|
<div
|
||||||
</span>
|
v-for="tier in tiers"
|
||||||
<span
|
:key="tier.id"
|
||||||
class="font-inter text-sm font-bold leading-normal text-base-foreground"
|
:class="
|
||||||
>
|
cn(
|
||||||
{{ tier.maxDuration }}
|
'flex-1 flex flex-col rounded-2xl border border-border-default bg-base-background shadow-[0_0_12px_rgba(0,0,0,0.1)]',
|
||||||
</span>
|
tier.isPopular ? 'border-muted-foreground' : ''
|
||||||
</div>
|
)
|
||||||
|
"
|
||||||
<div class="flex flex-row items-center justify-between">
|
>
|
||||||
<span class="text-sm font-normal text-muted-foreground">
|
<div class="p-8 pb-0 flex flex-col gap-8">
|
||||||
{{ t('subscription.gpuLabel') }}
|
<div class="flex flex-row items-center gap-2 justify-between">
|
||||||
</span>
|
<span
|
||||||
<i class="pi pi-check text-xs text-success-foreground" />
|
class="font-inter text-base font-bold leading-normal text-base-foreground"
|
||||||
</div>
|
>
|
||||||
|
{{ tier.name }}
|
||||||
<div class="flex flex-row items-center justify-between">
|
</span>
|
||||||
<span class="text-sm font-normal text-muted-foreground">
|
<div
|
||||||
{{ t('subscription.addCreditsLabel') }}
|
v-if="tier.isPopular"
|
||||||
</span>
|
class="rounded-full bg-base-foreground px-1.5 text-[11px] font-bold uppercase text-base-background h-5 tracking-tight flex items-center"
|
||||||
<i class="pi pi-check text-xs text-success-foreground" />
|
>
|
||||||
</div>
|
{{ t('subscription.mostPopular') }}
|
||||||
|
</div>
|
||||||
<div class="flex flex-row items-center justify-between">
|
</div>
|
||||||
<span class="text-sm font-normal text-muted-foreground">
|
<div class="flex flex-col">
|
||||||
{{ t('subscription.customLoRAsLabel') }}
|
|
||||||
</span>
|
|
||||||
<i
|
|
||||||
v-if="tier.customLoRAs"
|
|
||||||
class="pi pi-check text-xs text-success-foreground"
|
|
||||||
/>
|
|
||||||
<i v-else class="pi pi-times text-xs text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<div class="flex flex-row items-start justify-between">
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<span class="text-sm font-normal text-muted-foreground">
|
<div class="flex flex-row items-baseline gap-2">
|
||||||
{{ t('subscription.videoEstimateLabel') }}
|
|
||||||
</span>
|
|
||||||
<div class="flex flex-row items-center gap-2 opacity-50">
|
|
||||||
<i
|
|
||||||
class="pi pi-question-circle text-xs text-muted-foreground"
|
|
||||||
/>
|
|
||||||
<span
|
<span
|
||||||
class="text-sm font-normal text-muted-foreground cursor-pointer hover:text-base-foreground"
|
class="font-inter text-[32px] font-semibold leading-normal text-base-foreground"
|
||||||
@click="togglePopover"
|
|
||||||
>
|
>
|
||||||
{{ t('subscription.videoEstimateHelp') }}
|
<span
|
||||||
|
v-show="currentBillingCycle === 'yearly'"
|
||||||
|
class="line-through text-2xl text-muted-foreground"
|
||||||
|
>
|
||||||
|
${{ tier.price.monthly }}
|
||||||
|
</span>
|
||||||
|
${{ getPrice(tier) }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="font-inter text-xl leading-normal text-base-foreground"
|
||||||
|
>
|
||||||
|
{{ t('subscription.usdPerMonth') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm text-muted-foreground">
|
||||||
|
{{
|
||||||
|
currentBillingCycle === 'yearly'
|
||||||
|
? t('subscription.billedAnnually', {
|
||||||
|
total: tier.price.annualTotal
|
||||||
|
})
|
||||||
|
: t('subscription.billedMonthly')
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4 pb-0 flex-1">
|
||||||
|
<div class="flex flex-row items-center justify-between">
|
||||||
|
<span
|
||||||
|
class="font-inter text-sm font-normal leading-normal text-foreground"
|
||||||
|
>
|
||||||
|
{{ t('subscription.monthlyCreditsLabel') }}
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-row items-center gap-1">
|
||||||
|
<i class="icon-[lucide--component] text-amber-400 text-sm" />
|
||||||
|
<span
|
||||||
|
class="font-inter text-sm font-bold leading-normal text-base-foreground"
|
||||||
|
>
|
||||||
|
{{ tier.credits }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row items-center justify-between">
|
||||||
|
<span class="text-sm font-normal text-foreground">
|
||||||
|
{{ t('subscription.maxDurationLabel') }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="font-inter text-sm font-bold leading-normal text-base-foreground"
|
||||||
|
>
|
||||||
|
{{ tier.maxDuration }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row items-center justify-between">
|
||||||
|
<span class="text-sm font-normal text-foreground">
|
||||||
|
{{ t('subscription.gpuLabel') }}
|
||||||
|
</span>
|
||||||
|
<i class="pi pi-check text-xs text-success-foreground" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row items-center justify-between">
|
||||||
|
<span class="text-sm font-normal text-foreground">
|
||||||
|
{{ t('subscription.addCreditsLabel') }}
|
||||||
|
</span>
|
||||||
|
<i class="pi pi-check text-xs text-success-foreground" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row items-center justify-between">
|
||||||
|
<span class="text-sm font-normal text-foreground">
|
||||||
|
{{ t('subscription.customLoRAsLabel') }}
|
||||||
|
</span>
|
||||||
|
<i
|
||||||
|
v-if="tier.customLoRAs"
|
||||||
|
class="pi pi-check text-xs text-success-foreground"
|
||||||
|
/>
|
||||||
|
<i v-else class="pi pi-times text-xs text-foreground" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex flex-row items-start justify-between">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<span class="text-sm font-normal text-foreground">
|
||||||
|
{{ t('subscription.videoEstimateLabel') }}
|
||||||
|
</span>
|
||||||
|
<div class="flex flex-row items-center gap-2 group pt-2">
|
||||||
|
<i
|
||||||
|
class="pi pi-question-circle text-xs text-muted-foreground group-hover:text-base-foreground"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="text-sm font-normal text-muted-foreground cursor-pointer group-hover:text-base-foreground"
|
||||||
|
@click="togglePopover"
|
||||||
|
>
|
||||||
|
{{ t('subscription.videoEstimateHelp') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="font-inter text-sm font-bold leading-normal text-base-foreground"
|
||||||
|
>
|
||||||
|
{{ tier.videoEstimate }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span
|
|
||||||
class="font-inter text-sm font-bold leading-normal text-base-foreground"
|
|
||||||
>
|
|
||||||
{{ tier.videoEstimate }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="flex flex-col p-8">
|
||||||
|
<Button
|
||||||
<div class="flex flex-col p-8">
|
:label="getButtonLabel(tier)"
|
||||||
<Button
|
:severity="getButtonSeverity(tier)"
|
||||||
:label="getButtonLabel(tier)"
|
:disabled="isLoading || isCurrentPlan(tier.key)"
|
||||||
:severity="getButtonSeverity(tier)"
|
:loading="loadingTier === tier.key"
|
||||||
:disabled="isLoading || isCurrentPlan(tier.key)"
|
:class="
|
||||||
:loading="loadingTier === tier.key"
|
cn(
|
||||||
class="h-10 w-full"
|
'h-10 w-full',
|
||||||
:pt="{
|
tier.key === 'creator'
|
||||||
label: {
|
? 'bg-base-foreground border-transparent hover:bg-inverted-background-hover'
|
||||||
class: getButtonTextClass(tier)
|
: 'bg-secondary-background border-transparent hover:bg-secondary-background-hover focus:bg-secondary-background-selected'
|
||||||
}
|
)
|
||||||
}"
|
"
|
||||||
@click="() => handleSubscribe(tier.key)"
|
:pt="{
|
||||||
/>
|
label: {
|
||||||
|
class: getButtonTextClass(tier)
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
@click="() => handleSubscribe(tier.key)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Video Estimate Help Popover -->
|
||||||
|
<Popover
|
||||||
|
ref="popover"
|
||||||
|
append-to="body"
|
||||||
|
:auto-z-index="true"
|
||||||
|
:base-z-index="1000"
|
||||||
|
:dismissable="true"
|
||||||
|
:close-on-escape="true"
|
||||||
|
unstyled
|
||||||
|
:pt="{
|
||||||
|
root: {
|
||||||
|
class:
|
||||||
|
'rounded-lg border border-interface-stroke bg-interface-panel-surface shadow-lg p-4 max-w-xs'
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<p class="text-sm text-base-foreground">
|
||||||
|
{{ t('subscription.videoEstimateExplanation') }}
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="https://cloud.comfy.org/?template=video_wan2_2_14B_fun_camera"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="text-sm text-azure-600 hover:text-azure-400 underline"
|
||||||
|
>
|
||||||
|
{{ t('subscription.videoEstimateTryTemplate') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Video Estimate Help Popover -->
|
|
||||||
<Popover
|
|
||||||
ref="popover"
|
|
||||||
append-to="body"
|
|
||||||
:auto-z-index="true"
|
|
||||||
:base-z-index="1000"
|
|
||||||
:dismissable="true"
|
|
||||||
:close-on-escape="true"
|
|
||||||
unstyled
|
|
||||||
:pt="{
|
|
||||||
root: {
|
|
||||||
class:
|
|
||||||
'rounded-lg border border-interface-stroke bg-interface-panel-surface shadow-lg p-4 max-w-xs'
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<p class="text-sm text-base-foreground">
|
|
||||||
{{ t('subscription.videoEstimateExplanation') }}
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
href="https://cloud.comfy.org/?template=video_wan2_2_14B_fun_camera"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="text-sm text-azure-600 hover:text-azure-400 underline"
|
|
||||||
>
|
|
||||||
{{ t('subscription.videoEstimateTryTemplate') }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { cn } from '@comfyorg/tailwind-utils'
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import Popover from 'primevue/popover'
|
import Popover from 'primevue/popover'
|
||||||
|
import SelectButton from 'primevue/selectbutton'
|
||||||
|
import type { ToggleButtonPassThroughMethodOptions } from 'primevue/togglebutton'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||||
@@ -182,12 +257,25 @@ import type { components } from '@/types/comfyRegistryTypes'
|
|||||||
|
|
||||||
type SubscriptionTier = components['schemas']['SubscriptionTier']
|
type SubscriptionTier = components['schemas']['SubscriptionTier']
|
||||||
type TierKey = 'standard' | 'creator' | 'pro'
|
type TierKey = 'standard' | 'creator' | 'pro'
|
||||||
|
type CheckoutTier = TierKey | `${TierKey}-yearly`
|
||||||
|
|
||||||
|
type BillingCycle = 'monthly' | 'yearly'
|
||||||
|
|
||||||
|
const getCheckoutTier = (
|
||||||
|
tierKey: TierKey,
|
||||||
|
billingCycle: BillingCycle
|
||||||
|
): CheckoutTier => (billingCycle === 'yearly' ? `${tierKey}-yearly` : tierKey)
|
||||||
|
|
||||||
|
interface BillingCycleOption {
|
||||||
|
label: string
|
||||||
|
value: BillingCycle
|
||||||
|
}
|
||||||
|
|
||||||
interface PricingTierConfig {
|
interface PricingTierConfig {
|
||||||
id: SubscriptionTier
|
id: SubscriptionTier
|
||||||
key: TierKey
|
key: TierKey
|
||||||
name: string
|
name: string
|
||||||
price: string
|
price: Record<BillingCycle, string> & { annualTotal: string }
|
||||||
credits: string
|
credits: string
|
||||||
maxDuration: string
|
maxDuration: string
|
||||||
customLoRAs: boolean
|
customLoRAs: boolean
|
||||||
@@ -195,6 +283,11 @@ interface PricingTierConfig {
|
|||||||
isPopular?: boolean
|
isPopular?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const billingCycleOptions: BillingCycleOption[] = [
|
||||||
|
{ label: t('subscription.yearly'), value: 'yearly' },
|
||||||
|
{ label: t('subscription.monthly'), value: 'monthly' }
|
||||||
|
]
|
||||||
|
|
||||||
const TIER_TO_KEY: Record<SubscriptionTier, TierKey> = {
|
const TIER_TO_KEY: Record<SubscriptionTier, TierKey> = {
|
||||||
STANDARD: 'standard',
|
STANDARD: 'standard',
|
||||||
CREATOR: 'creator',
|
CREATOR: 'creator',
|
||||||
@@ -207,7 +300,11 @@ const tiers: PricingTierConfig[] = [
|
|||||||
id: 'STANDARD',
|
id: 'STANDARD',
|
||||||
key: 'standard',
|
key: 'standard',
|
||||||
name: t('subscription.tiers.standard.name'),
|
name: t('subscription.tiers.standard.name'),
|
||||||
price: t('subscription.tiers.standard.price'),
|
price: {
|
||||||
|
monthly: t('subscription.tiers.standard.price.monthly'),
|
||||||
|
yearly: t('subscription.tiers.standard.price.yearly'),
|
||||||
|
annualTotal: t('subscription.tiers.standard.price.annualTotal')
|
||||||
|
},
|
||||||
credits: t('subscription.credits.standard'),
|
credits: t('subscription.credits.standard'),
|
||||||
maxDuration: t('subscription.maxDuration.standard'),
|
maxDuration: t('subscription.maxDuration.standard'),
|
||||||
customLoRAs: false,
|
customLoRAs: false,
|
||||||
@@ -218,7 +315,11 @@ const tiers: PricingTierConfig[] = [
|
|||||||
id: 'CREATOR',
|
id: 'CREATOR',
|
||||||
key: 'creator',
|
key: 'creator',
|
||||||
name: t('subscription.tiers.creator.name'),
|
name: t('subscription.tiers.creator.name'),
|
||||||
price: t('subscription.tiers.creator.price'),
|
price: {
|
||||||
|
monthly: t('subscription.tiers.creator.price.monthly'),
|
||||||
|
yearly: t('subscription.tiers.creator.price.yearly'),
|
||||||
|
annualTotal: t('subscription.tiers.creator.price.annualTotal')
|
||||||
|
},
|
||||||
credits: t('subscription.credits.creator'),
|
credits: t('subscription.credits.creator'),
|
||||||
maxDuration: t('subscription.maxDuration.creator'),
|
maxDuration: t('subscription.maxDuration.creator'),
|
||||||
customLoRAs: true,
|
customLoRAs: true,
|
||||||
@@ -229,7 +330,11 @@ const tiers: PricingTierConfig[] = [
|
|||||||
id: 'PRO',
|
id: 'PRO',
|
||||||
key: 'pro',
|
key: 'pro',
|
||||||
name: t('subscription.tiers.pro.name'),
|
name: t('subscription.tiers.pro.name'),
|
||||||
price: t('subscription.tiers.pro.price'),
|
price: {
|
||||||
|
monthly: t('subscription.tiers.pro.price.monthly'),
|
||||||
|
yearly: t('subscription.tiers.pro.price.yearly'),
|
||||||
|
annualTotal: t('subscription.tiers.pro.price.annualTotal')
|
||||||
|
},
|
||||||
credits: t('subscription.credits.pro'),
|
credits: t('subscription.credits.pro'),
|
||||||
maxDuration: t('subscription.maxDuration.pro'),
|
maxDuration: t('subscription.maxDuration.pro'),
|
||||||
customLoRAs: true,
|
customLoRAs: true,
|
||||||
@@ -246,6 +351,7 @@ const { wrapWithErrorHandlingAsync } = useErrorHandling()
|
|||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
const loadingTier = ref<TierKey | null>(null)
|
const loadingTier = ref<TierKey | null>(null)
|
||||||
const popover = ref()
|
const popover = ref()
|
||||||
|
const currentBillingCycle = ref<BillingCycle>('yearly')
|
||||||
|
|
||||||
const currentTierKey = computed<TierKey | null>(() =>
|
const currentTierKey = computed<TierKey | null>(() =>
|
||||||
subscriptionTier.value ? TIER_TO_KEY[subscriptionTier.value] : null
|
subscriptionTier.value ? TIER_TO_KEY[subscriptionTier.value] : null
|
||||||
@@ -274,17 +380,21 @@ const getButtonSeverity = (tier: PricingTierConfig): 'primary' | 'secondary' =>
|
|||||||
|
|
||||||
const getButtonTextClass = (tier: PricingTierConfig): string =>
|
const getButtonTextClass = (tier: PricingTierConfig): string =>
|
||||||
tier.key === 'creator'
|
tier.key === 'creator'
|
||||||
? 'font-inter text-sm font-bold leading-normal text-white'
|
? 'font-inter text-sm font-bold leading-normal text-base-background'
|
||||||
: 'font-inter text-sm font-bold leading-normal text-primary-foreground'
|
: 'font-inter text-sm font-bold leading-normal text-primary-foreground'
|
||||||
|
|
||||||
|
const getPrice = (tier: PricingTierConfig): string =>
|
||||||
|
tier.price[currentBillingCycle.value]
|
||||||
|
|
||||||
const initiateCheckout = async (tierKey: TierKey) => {
|
const initiateCheckout = async (tierKey: TierKey) => {
|
||||||
const authHeader = await getAuthHeader()
|
const authHeader = await getAuthHeader()
|
||||||
if (!authHeader) {
|
if (!authHeader) {
|
||||||
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const checkoutTier = getCheckoutTier(tierKey, currentBillingCycle.value)
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${getComfyApiBaseUrl()}/customers/cloud-subscription-checkout/${tierKey}`,
|
`${getComfyApiBaseUrl()}/customers/cloud-subscription-checkout/${checkoutTier}`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { ...authHeader, 'Content-Type': 'application/json' }
|
headers: { ...authHeader, 'Content-Type': 'application/json' }
|
||||||
|
|||||||
@@ -1,45 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="showCustomPricingTable"
|
v-if="showCustomPricingTable"
|
||||||
class="flex flex-col gap-6 rounded-[24px] border border-interface-stroke bg-[var(--p-dialog-background)] p-4 shadow-[0_25px_80px_rgba(5,6,12,0.45)] md:p-6"
|
class="relative flex flex-col p-4 pt-8 md:p-16 !overflow-y-auto h-full gap-8"
|
||||||
>
|
>
|
||||||
<div
|
<Button
|
||||||
class="flex flex-col gap-6 md:flex-row md:items-start md:justify-between"
|
:pt="{
|
||||||
>
|
icon: { class: 'text-xl' }
|
||||||
<div class="flex flex-col gap-2 text-left md:max-w-2xl">
|
}"
|
||||||
<div
|
icon="pi pi-times"
|
||||||
class="inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.3em] text-text-secondary"
|
text
|
||||||
>
|
rounded
|
||||||
{{ $t('subscription.required.title') }}
|
class="shrink-0 text-text-secondary hover:bg-white/10 absolute right-2.5 top-2.5"
|
||||||
<CloudBadge
|
:aria-label="$t('g.close')"
|
||||||
reverse-order
|
@click="handleClose"
|
||||||
no-padding
|
/>
|
||||||
background-color="var(--p-dialog-background)"
|
<div class="text-center">
|
||||||
use-subscription
|
<h2 class="text-xl lg:text-2xl text-muted-foreground m-0">
|
||||||
/>
|
{{ $t('subscription.description') }}
|
||||||
</div>
|
</h2>
|
||||||
<div class="text-3xl font-semibold leading-tight md:text-4xl">
|
|
||||||
{{ $t('subscription.description') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
icon="pi pi-times"
|
|
||||||
text
|
|
||||||
rounded
|
|
||||||
class="h-10 w-10 shrink-0 text-text-secondary hover:bg-white/10"
|
|
||||||
:aria-label="$t('g.close')"
|
|
||||||
@click="handleClose"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PricingTable class="flex-1" />
|
<PricingTable class="flex-1" />
|
||||||
|
|
||||||
<!-- Contact and Enterprise Links -->
|
<!-- Contact and Enterprise Links -->
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center gap-2">
|
||||||
<p class="text-sm text-text-secondary">
|
<p class="text-sm text-text-secondary m-0">
|
||||||
{{ $t('subscription.haveQuestions') }}
|
{{ $t('subscription.haveQuestions') }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-1.5">
|
||||||
<Button
|
<Button
|
||||||
:label="$t('subscription.contactUs')"
|
:label="$t('subscription.contactUs')"
|
||||||
text
|
text
|
||||||
@@ -95,7 +83,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex flex-col gap-6">
|
<div class="flex flex-col gap-6">
|
||||||
<div class="inline-flex items-center gap-2">
|
<div class="inline-flex items-center gap-2">
|
||||||
<div class="text-sm text-muted text-text-primary">
|
<div class="text-sm text-text-primary">
|
||||||
{{ $t('subscription.required.title') }}
|
{{ $t('subscription.required.title') }}
|
||||||
</div>
|
</div>
|
||||||
<CloudBadge
|
<CloudBadge
|
||||||
|
|||||||
@@ -23,13 +23,14 @@ export const useSubscriptionDialog = () => {
|
|||||||
onClose: hide
|
onClose: hide
|
||||||
},
|
},
|
||||||
dialogComponentProps: {
|
dialogComponentProps: {
|
||||||
style: 'width: min(1200px, 95vw); max-height: 90vh;',
|
style: 'width: min(1328px, 95vw); max-height: 90vh;',
|
||||||
pt: {
|
pt: {
|
||||||
root: {
|
root: {
|
||||||
class: '!rounded-[32px] overflow-visible'
|
class: 'rounded-2xl bg-transparent'
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
class: '!p-0 bg-transparent'
|
class:
|
||||||
|
'!p-0 rounded-2xl border border-border-default bg-base-background/60 backdrop-blur-md shadow-[0_25px_80px_rgba(5,6,12,0.45)]'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user