Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
131 changes: 95 additions & 36 deletions src/platform/cloud/subscription/components/PricingTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,23 +20,31 @@
</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>
</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">
<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>
Expand All @@ -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>
Expand Down Expand Up @@ -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>
Expand All @@ -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'
Expand Down Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -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>