feat: add popover with link to Wan Fun Control template on pricing table (#7363)

## Summary
- Add clickable popover to the "What is this?" help text in video
estimates
- Explains that estimates are based on the Wan Fun Control template for
5-second videos
- Includes direct link to try the template:
`cloud.comfy.org/?template=video_wan2_2_14B_fun_camera`

This improves user understanding of how video estimates are calculated
and provides easy access to try the template that the estimates are
based on.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7363-feat-add-popover-with-link-to-Wan-Fun-Control-template-on-pricing-table-2c66d73d36508109b7a6ef80f978448e)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Christian Byrne
2025-12-11 01:38:47 -08:00
committed by GitHub
parent f5f0e20332
commit c8f88d5ba7
2 changed files with 97 additions and 36 deletions

View File

@@ -2011,6 +2011,8 @@
"customLoRAsLabel": "Import your own LoRAs",
"videoEstimateLabel": "Approx. amount of 5s videos generated with Wan Fun Control template",
"videoEstimateHelp": "What is this?",
"videoEstimateExplanation": "These estimates are based on the Wan Fun Control template for 5-second videos.",
"videoEstimateTryTemplate": "Try the Wan Fun Control template →",
"upgradeTo": "Upgrade to {plan}",
"changeTo": "Change to {plan}",
"credits": {

View File

@@ -7,7 +7,9 @@
>
<div class="flex flex-col gap-6 p-8">
<div class="flex flex-row items-center gap-2">
<span class="font-inter text-base font-bold leading-normal text-base-foreground">
<span
class="font-inter text-base font-bold leading-normal text-base-foreground"
>
{{ tier.name }}
</span>
<div
@@ -18,10 +20,14 @@
</div>
</div>
<div class="flex flex-row items-baseline gap-2">
<span class="font-inter text-[32px] font-semibold leading-normal text-base-foreground">
<span
class="font-inter text-[32px] font-semibold leading-normal text-base-foreground"
>
${{ tier.price }}
</span>
<span class="font-inter text-base font-normal leading-normal text-base-foreground">
<span
class="font-inter text-base font-normal leading-normal text-base-foreground"
>
{{ t('subscription.usdPerMonth') }}
</span>
</div>
@@ -29,12 +35,16 @@
<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">
<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">
<span
class="font-inter text-sm font-bold leading-normal text-base-foreground"
>
{{ tier.credits }}
</span>
</div>
@@ -44,7 +54,9 @@
<span class="text-sm font-normal text-muted-foreground">
{{ t('subscription.maxDurationLabel') }}
</span>
<span class="font-inter text-sm font-bold leading-normal text-base-foreground">
<span
class="font-inter text-sm font-bold leading-normal text-base-foreground"
>
{{ tier.maxDuration }}
</span>
</div>
@@ -78,13 +90,20 @@
{{ 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 class="text-sm font-normal text-muted-foreground">
<i
class="pi pi-question-circle text-xs text-muted-foreground"
/>
<span
class="text-sm font-normal text-muted-foreground cursor-pointer 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">
<span
class="font-inter text-sm font-bold leading-normal text-base-foreground"
>
{{ tier.videoEstimate }}
</span>
</div>
@@ -108,10 +127,42 @@
</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="http://cloud.comfy.org/?template=video_wan2_2_14B_fun_camera"
target="_blank"
rel="noopener noreferrer"
class="text-sm text-blue-500 hover:text-blue-400 underline"
>
{{ t('subscription.videoEstimateTryTemplate') }}
</a>
</div>
</Popover>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Popover from 'primevue/popover'
import { computed, ref } from 'vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
@@ -191,22 +242,32 @@ const { wrapWithErrorHandlingAsync } = useErrorHandling()
const isLoading = ref(false)
const loadingTier = ref<TierKey | null>(null)
const popover = ref()
const currentTierKey = computed<TierKey | null>(() =>
const currentTierKey = computed<TierKey | null>(() =>
subscriptionTier.value ? TIER_TO_KEY[subscriptionTier.value] : null
)
const isCurrentPlan = (tierKey: TierKey): boolean =>
const isCurrentPlan = (tierKey: TierKey): boolean =>
currentTierKey.value === tierKey
const togglePopover = (event: Event) => {
popover.value.toggle(event)
}
const getButtonLabel = (tier: PricingTierConfig): string => {
if (isCurrentPlan(tier.key)) return t('subscription.currentPlan')
if (!isActiveSubscription.value) return t('subscription.subscribeTo', { plan: tier.name })
if (!isActiveSubscription.value)
return t('subscription.subscribeTo', { plan: tier.name })
return t('subscription.changeTo', { plan: tier.name })
}
const getButtonSeverity = (tier: PricingTierConfig): 'primary' | 'secondary' =>
isCurrentPlan(tier.key) ? 'secondary' : tier.key === 'creator' ? 'primary' : 'secondary'
const getButtonSeverity = (tier: PricingTierConfig): 'primary' | 'secondary' =>
isCurrentPlan(tier.key)
? 'secondary'
: tier.key === 'creator'
? 'primary'
: 'secondary'
const initiateCheckout = async (tierKey: TierKey) => {
const authHeader = await getAuthHeader()
@@ -231,12 +292,13 @@ const initiateCheckout = async (tierKey: TierKey) => {
// If JSON parsing fails, try to get text response or use HTTP status
try {
const errorText = await response.text()
errorMessage = errorText || `HTTP ${response.status} ${response.statusText}`
errorMessage =
errorText || `HTTP ${response.status} ${response.statusText}`
} catch {
errorMessage = `HTTP ${response.status} ${response.statusText}`
}
}
throw new FirebaseAuthStoreError(
t('toastMessages.failedToInitiateSubscription', {
error: errorMessage
@@ -247,27 +309,24 @@ const initiateCheckout = async (tierKey: TierKey) => {
return await response.json()
}
const handleSubscribe = wrapWithErrorHandlingAsync(
async (tierKey: TierKey) => {
if (!isCloud || isLoading.value || isCurrentPlan(tierKey)) return
const handleSubscribe = wrapWithErrorHandlingAsync(async (tierKey: TierKey) => {
if (!isCloud || isLoading.value || isCurrentPlan(tierKey)) return
isLoading.value = true
loadingTier.value = tierKey
isLoading.value = true
loadingTier.value = tierKey
try {
if (isActiveSubscription.value) {
await accessBillingPortal()
} else {
const response = await initiateCheckout(tierKey)
if (response.checkout_url) {
window.open(response.checkout_url, '_blank')
}
try {
if (isActiveSubscription.value) {
await accessBillingPortal()
} else {
const response = await initiateCheckout(tierKey)
if (response.checkout_url) {
window.open(response.checkout_url, '_blank')
}
} finally {
isLoading.value = false
loadingTier.value = null
}
},
reportError
)
</script>
} finally {
isLoading.value = false
loadingTier.value = null
}
}, reportError)
</script>