mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
refactor: start on removing FF for subscription tiers (#7596)
## Summary Refactor: remove FF for subscription tier, remove legacy code for non subscription tier logic. ## Review Focus Preexisting cloud functionality impact. <!-- If this PR fixes an issue, uncomment and update the line below --> <!-- Fixes #ISSUE_NUMBER --> ## Screenshots (if applicable) <!-- Add screenshots or video recording to help explain your changes --> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7596-refactor-start-on-removing-FF-for-subscription-tiers-2cc6d73d3650816bac3aef893e4f37cd) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -14,13 +14,7 @@
|
|||||||
class="p-1 text-amber-400"
|
class="p-1 text-amber-400"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<i
|
<i class="icon-[lucide--component]" />
|
||||||
:class="
|
|
||||||
flags.subscriptionTiersEnabled
|
|
||||||
? 'icon-[lucide--component]'
|
|
||||||
: 'pi pi-dollar'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</Tag>
|
</Tag>
|
||||||
<div :class="textClass">
|
<div :class="textClass">
|
||||||
@@ -36,7 +30,6 @@ import { computed } from 'vue'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import { formatCreditsFromCents } from '@/base/credits/comfyCredits'
|
import { formatCreditsFromCents } from '@/base/credits/comfyCredits'
|
||||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
|
||||||
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
|
||||||
|
|
||||||
const { textClass, showCreditsOnly } = defineProps<{
|
const { textClass, showCreditsOnly } = defineProps<{
|
||||||
@@ -45,7 +38,6 @@ const { textClass, showCreditsOnly } = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const authStore = useFirebaseAuthStore()
|
const authStore = useFirebaseAuthStore()
|
||||||
const { flags } = useFeatureFlags()
|
|
||||||
const balanceLoading = computed(() => authStore.isFetchingBalance)
|
const balanceLoading = computed(() => authStore.isFetchingBalance)
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- New Credits Design (default) -->
|
<div class="flex w-112 flex-col gap-8 p-8">
|
||||||
<div v-if="useNewDesign" class="flex w-112 flex-col gap-8 p-8">
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<h1 class="text-2xl font-semibold text-base-foreground m-0">
|
<h1 class="text-2xl font-semibold text-base-foreground m-0">
|
||||||
@@ -66,91 +65,32 @@
|
|||||||
@click="handleBuy"
|
@click="handleBuy"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Legacy Design -->
|
|
||||||
<div v-else class="flex w-96 flex-col gap-10 p-2">
|
|
||||||
<div v-if="isInsufficientCredits" class="flex flex-col gap-4">
|
|
||||||
<h1 class="my-0 text-2xl leading-normal font-medium">
|
|
||||||
{{ $t('credits.topUp.insufficientTitle') }}
|
|
||||||
</h1>
|
|
||||||
<p class="my-0 text-base">
|
|
||||||
{{ $t('credits.topUp.insufficientMessage') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Balance Section -->
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex w-full flex-col gap-2">
|
|
||||||
<div class="text-base text-muted">
|
|
||||||
{{ $t('credits.yourCreditBalance') }}
|
|
||||||
</div>
|
|
||||||
<div class="flex w-full items-center justify-between">
|
|
||||||
<UserCredit text-class="text-2xl" />
|
|
||||||
<Button
|
|
||||||
outlined
|
|
||||||
severity="secondary"
|
|
||||||
:label="$t('credits.topUp.seeDetails')"
|
|
||||||
icon="pi pi-arrow-up-right"
|
|
||||||
@click="handleSeeDetails"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Amount Input Section -->
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<span class="text-sm text-muted"
|
|
||||||
>{{ $t('credits.topUp.quickPurchase') }}:</span
|
|
||||||
>
|
|
||||||
<div class="grid grid-cols-[2fr_1fr] gap-2">
|
|
||||||
<LegacyCreditTopUpOption
|
|
||||||
v-for="amount in amountOptions"
|
|
||||||
:key="amount"
|
|
||||||
:amount="amount"
|
|
||||||
:preselected="amount === preselectedAmountOption"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<LegacyCreditTopUpOption :amount="100" :preselected="false" editable />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import { useToast } from 'primevue/usetoast'
|
import { useToast } from 'primevue/usetoast'
|
||||||
import { computed, ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import { creditsToUsd } from '@/base/credits/comfyCredits'
|
import { creditsToUsd } from '@/base/credits/comfyCredits'
|
||||||
import UserCredit from '@/components/common/UserCredit.vue'
|
import UserCredit from '@/components/common/UserCredit.vue'
|
||||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
|
||||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||||
import { useTelemetry } from '@/platform/telemetry'
|
import { useTelemetry } from '@/platform/telemetry'
|
||||||
|
|
||||||
import CreditTopUpOption from './credit/CreditTopUpOption.vue'
|
import CreditTopUpOption from './credit/CreditTopUpOption.vue'
|
||||||
import LegacyCreditTopUpOption from './credit/LegacyCreditTopUpOption.vue'
|
|
||||||
|
|
||||||
interface CreditOption {
|
interface CreditOption {
|
||||||
credits: number
|
credits: number
|
||||||
description: string
|
description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { isInsufficientCredits = false } = defineProps<{
|
||||||
isInsufficientCredits = false,
|
|
||||||
amountOptions = [5, 10, 20, 50],
|
|
||||||
preselectedAmountOption = 10
|
|
||||||
} = defineProps<{
|
|
||||||
isInsufficientCredits?: boolean
|
isInsufficientCredits?: boolean
|
||||||
amountOptions?: number[]
|
|
||||||
preselectedAmountOption?: number
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { flags } = useFeatureFlags()
|
|
||||||
const { formattedRenewalDate } = useSubscription()
|
const { formattedRenewalDate } = useSubscription()
|
||||||
// Use feature flag to determine design - defaults to true (new design)
|
|
||||||
const useNewDesign = computed(() => flags.subscriptionTiersEnabled)
|
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const authActions = useFirebaseAuthActions()
|
const authActions = useFirebaseAuthActions()
|
||||||
@@ -202,8 +142,4 @@ const handleBuy = async () => {
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSeeDetails = async () => {
|
|
||||||
await authActions.accessBillingPortal()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,119 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Tag
|
|
||||||
severity="secondary"
|
|
||||||
icon="pi pi-wallet"
|
|
||||||
rounded
|
|
||||||
class="p-1 text-amber-400"
|
|
||||||
/>
|
|
||||||
<div v-if="editable" class="flex items-center gap-2">
|
|
||||||
<InputNumber
|
|
||||||
v-model="customAmount"
|
|
||||||
:min="1"
|
|
||||||
:max="1000"
|
|
||||||
:step="1"
|
|
||||||
show-buttons
|
|
||||||
:allow-empty="false"
|
|
||||||
:highlight-on-focus="true"
|
|
||||||
prefix="$"
|
|
||||||
pt:pc-input-text:root="w-28"
|
|
||||||
@blur="
|
|
||||||
(e: InputNumberBlurEvent) =>
|
|
||||||
(customAmount = clampUsd(Number(e.value)))
|
|
||||||
"
|
|
||||||
@input="
|
|
||||||
(e: InputNumberInputEvent) =>
|
|
||||||
(customAmount = clampUsd(Number(e.value)))
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<span class="text-xs text-muted">{{ formattedCredits }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex flex-col leading-tight">
|
|
||||||
<span class="text-xl font-semibold">{{ formattedCredits }}</span>
|
|
||||||
<span class="text-xs text-muted">{{ formattedUsd }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ProgressSpinner v-if="loading" class="h-8 w-8" />
|
|
||||||
<Button
|
|
||||||
v-else
|
|
||||||
:severity="preselected ? 'primary' : 'secondary'"
|
|
||||||
:outlined="!preselected"
|
|
||||||
:label="$t('credits.topUp.buyNow')"
|
|
||||||
@click="handleBuyNow"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import Button from 'primevue/button'
|
|
||||||
import InputNumber from 'primevue/inputnumber'
|
|
||||||
import type {
|
|
||||||
InputNumberBlurEvent,
|
|
||||||
InputNumberInputEvent
|
|
||||||
} from 'primevue/inputnumber'
|
|
||||||
import ProgressSpinner from 'primevue/progressspinner'
|
|
||||||
import Tag from 'primevue/tag'
|
|
||||||
import { computed, onBeforeUnmount, ref } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
import {
|
|
||||||
clampUsd,
|
|
||||||
formatCreditsFromUsd,
|
|
||||||
formatUsd
|
|
||||||
} from '@/base/credits/comfyCredits'
|
|
||||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
|
||||||
import { useTelemetry } from '@/platform/telemetry'
|
|
||||||
|
|
||||||
const authActions = useFirebaseAuthActions()
|
|
||||||
const telemetry = useTelemetry()
|
|
||||||
|
|
||||||
const {
|
|
||||||
amount,
|
|
||||||
preselected,
|
|
||||||
editable = false
|
|
||||||
} = defineProps<{
|
|
||||||
amount: number
|
|
||||||
preselected: boolean
|
|
||||||
editable?: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const customAmount = ref(amount)
|
|
||||||
const didClickBuyNow = ref(false)
|
|
||||||
const loading = ref(false)
|
|
||||||
const { t, locale } = useI18n()
|
|
||||||
|
|
||||||
const displayUsdAmount = computed(() =>
|
|
||||||
editable ? clampUsd(Number(customAmount.value)) : clampUsd(amount)
|
|
||||||
)
|
|
||||||
|
|
||||||
const formattedCredits = computed(
|
|
||||||
() =>
|
|
||||||
`${formatCreditsFromUsd({
|
|
||||||
usd: displayUsdAmount.value,
|
|
||||||
locale: locale.value
|
|
||||||
})} ${t('credits.credits')}`
|
|
||||||
)
|
|
||||||
|
|
||||||
const formattedUsd = computed(
|
|
||||||
() => `$${formatUsd({ value: displayUsdAmount.value, locale: locale.value })}`
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleBuyNow = async () => {
|
|
||||||
const creditAmount = displayUsdAmount.value
|
|
||||||
telemetry?.trackApiCreditTopupButtonPurchaseClicked(creditAmount)
|
|
||||||
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
await authActions.purchaseCredits(creditAmount)
|
|
||||||
didClickBuyNow.value = true
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (didClickBuyNow.value) {
|
|
||||||
// If clicked buy now, then returned back to the dialog and closed, fetch the balance
|
|
||||||
void authActions.fetchBalance()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -138,15 +138,6 @@ vi.mock('@/composables/useExternalLink', () => ({
|
|||||||
}))
|
}))
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Mock useFeatureFlags
|
|
||||||
vi.mock('@/composables/useFeatureFlags', () => ({
|
|
||||||
useFeatureFlags: vi.fn(() => ({
|
|
||||||
flags: {
|
|
||||||
subscriptionTiersEnabled: true
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Mock useTelemetry
|
// Mock useTelemetry
|
||||||
vi.mock('@/platform/telemetry', () => ({
|
vi.mock('@/platform/telemetry', () => ({
|
||||||
useTelemetry: vi.fn(() => ({
|
useTelemetry: vi.fn(() => ({
|
||||||
|
|||||||
@@ -42,7 +42,6 @@
|
|||||||
formattedBalance
|
formattedBalance
|
||||||
}}</span>
|
}}</span>
|
||||||
<i
|
<i
|
||||||
v-if="flags.subscriptionTiersEnabled"
|
|
||||||
v-tooltip="{ value: $t('credits.unified.tooltip'), showDelay: 300 }"
|
v-tooltip="{ value: $t('credits.unified.tooltip'), showDelay: 300 }"
|
||||||
class="icon-[lucide--circle-help] cursor-help text-base text-muted-foreground mr-auto"
|
class="icon-[lucide--circle-help] cursor-help text-base text-muted-foreground mr-auto"
|
||||||
/>
|
/>
|
||||||
@@ -147,7 +146,6 @@ import UserAvatar from '@/components/common/UserAvatar.vue'
|
|||||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||||
import { useExternalLink } from '@/composables/useExternalLink'
|
import { useExternalLink } from '@/composables/useExternalLink'
|
||||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
|
||||||
import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue'
|
import SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue'
|
||||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||||
import { useSubscriptionDialog } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
|
import { useSubscriptionDialog } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'
|
||||||
@@ -174,7 +172,6 @@ const {
|
|||||||
fetchStatus
|
fetchStatus
|
||||||
} = useSubscription()
|
} = useSubscription()
|
||||||
const subscriptionDialog = useSubscriptionDialog()
|
const subscriptionDialog = useSubscriptionDialog()
|
||||||
const { flags } = useFeatureFlags()
|
|
||||||
const { locale } = useI18n()
|
const { locale } = useI18n()
|
||||||
|
|
||||||
const formattedBalance = computed(() => {
|
const formattedBalance = computed(() => {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||||
import { LGraphBadge } from '@/lib/litegraph/src/litegraph'
|
import { LGraphBadge } from '@/lib/litegraph/src/litegraph'
|
||||||
|
|
||||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
|
||||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||||
import { adjustColor } from '@/utils/colorUtil'
|
import { adjustColor } from '@/utils/colorUtil'
|
||||||
|
|
||||||
@@ -10,7 +9,6 @@ componentIconSvg.src =
|
|||||||
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='oklch(83.01%25 0.163 83.16)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M15.536 11.293a1 1 0 0 0 0 1.414l2.376 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0zm-13.239 0a1 1 0 0 0 0 1.414l2.377 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414L6.088 8.916a1 1 0 0 0-1.414 0zm6.619 6.619a1 1 0 0 0 0 1.415l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.415l-2.377-2.376a1 1 0 0 0-1.414 0zm0-13.238a1 1 0 0 0 0 1.414l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z'/%3E%3C/svg%3E"
|
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='oklch(83.01%25 0.163 83.16)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M15.536 11.293a1 1 0 0 0 0 1.414l2.376 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0zm-13.239 0a1 1 0 0 0 0 1.414l2.377 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414L6.088 8.916a1 1 0 0 0-1.414 0zm6.619 6.619a1 1 0 0 0 0 1.415l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.415l-2.377-2.376a1 1 0 0 0-1.414 0zm0-13.238a1 1 0 0 0 0 1.414l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z'/%3E%3C/svg%3E"
|
||||||
|
|
||||||
export const usePriceBadge = () => {
|
export const usePriceBadge = () => {
|
||||||
const { flags } = useFeatureFlags()
|
|
||||||
function updateSubgraphCredits(node: LGraphNode) {
|
function updateSubgraphCredits(node: LGraphNode) {
|
||||||
if (!node.isSubgraphNode()) return
|
if (!node.isSubgraphNode()) return
|
||||||
node.badges = node.badges.filter((b) => !isCreditsBadge(b))
|
node.badges = node.badges.filter((b) => !isCreditsBadge(b))
|
||||||
@@ -40,53 +38,26 @@ export const usePriceBadge = () => {
|
|||||||
|
|
||||||
function isCreditsBadge(badge: LGraphBadge | (() => LGraphBadge)): boolean {
|
function isCreditsBadge(badge: LGraphBadge | (() => LGraphBadge)): boolean {
|
||||||
const badgeInstance = typeof badge === 'function' ? badge() : badge
|
const badgeInstance = typeof badge === 'function' ? badge() : badge
|
||||||
if (flags.subscriptionTiersEnabled) {
|
return badgeInstance.icon?.image === componentIconSvg
|
||||||
return badgeInstance.icon?.image === componentIconSvg
|
|
||||||
} else {
|
|
||||||
return badgeInstance.icon?.unicode === '\ue96b'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const colorPaletteStore = useColorPaletteStore()
|
const colorPaletteStore = useColorPaletteStore()
|
||||||
function getCreditsBadge(price: string): LGraphBadge {
|
function getCreditsBadge(price: string): LGraphBadge {
|
||||||
const isLightTheme = colorPaletteStore.completedActivePalette.light_theme
|
const isLightTheme = colorPaletteStore.completedActivePalette.light_theme
|
||||||
|
|
||||||
if (flags.subscriptionTiersEnabled) {
|
return new LGraphBadge({
|
||||||
return new LGraphBadge({
|
text: price,
|
||||||
text: price,
|
iconOptions: {
|
||||||
iconOptions: {
|
image: componentIconSvg,
|
||||||
image: componentIconSvg,
|
size: 8
|
||||||
size: 8
|
},
|
||||||
},
|
fgColor:
|
||||||
fgColor:
|
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
||||||
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
.BADGE_FG_COLOR,
|
||||||
.BADGE_FG_COLOR,
|
bgColor: isLightTheme
|
||||||
bgColor: isLightTheme
|
? adjustColor('#8D6932', { lightness: 0.5 })
|
||||||
? adjustColor('#8D6932', { lightness: 0.5 })
|
: '#8D6932'
|
||||||
: '#8D6932'
|
})
|
||||||
})
|
|
||||||
} else {
|
|
||||||
return new LGraphBadge({
|
|
||||||
text: price,
|
|
||||||
iconOptions: {
|
|
||||||
unicode: '\ue96b',
|
|
||||||
fontFamily: 'PrimeIcons',
|
|
||||||
color: isLightTheme
|
|
||||||
? adjustColor('#FABC25', { lightness: 0.5 })
|
|
||||||
: '#FABC25',
|
|
||||||
bgColor: isLightTheme
|
|
||||||
? adjustColor('#654020', { lightness: 0.5 })
|
|
||||||
: '#654020',
|
|
||||||
fontSize: 8
|
|
||||||
},
|
|
||||||
fgColor:
|
|
||||||
colorPaletteStore.completedActivePalette.colors.litegraph_base
|
|
||||||
.BADGE_FG_COLOR,
|
|
||||||
bgColor: isLightTheme
|
|
||||||
? adjustColor('#8D6932', { lightness: 0.5 })
|
|
||||||
: '#8D6932'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
getCreditsBadge,
|
getCreditsBadge,
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export enum ServerFeatureFlag {
|
|||||||
MODEL_UPLOAD_BUTTON_ENABLED = 'model_upload_button_enabled',
|
MODEL_UPLOAD_BUTTON_ENABLED = 'model_upload_button_enabled',
|
||||||
ASSET_UPDATE_OPTIONS_ENABLED = 'asset_update_options_enabled',
|
ASSET_UPDATE_OPTIONS_ENABLED = 'asset_update_options_enabled',
|
||||||
PRIVATE_MODELS_ENABLED = 'private_models_enabled',
|
PRIVATE_MODELS_ENABLED = 'private_models_enabled',
|
||||||
SUBSCRIPTION_TIERS_ENABLED = 'subscription_tiers_enabled',
|
|
||||||
ONBOARDING_SURVEY_ENABLED = 'onboarding_survey_enabled'
|
ONBOARDING_SURVEY_ENABLED = 'onboarding_survey_enabled'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,16 +57,6 @@ export function useFeatureFlags() {
|
|||||||
api.getServerFeature(ServerFeatureFlag.PRIVATE_MODELS_ENABLED, false)
|
api.getServerFeature(ServerFeatureFlag.PRIVATE_MODELS_ENABLED, false)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
get subscriptionTiersEnabled() {
|
|
||||||
// Check remote config first (from /api/features), fall back to websocket feature flags
|
|
||||||
return (
|
|
||||||
remoteConfig.value.subscription_tiers_enabled ??
|
|
||||||
api.getServerFeature(
|
|
||||||
ServerFeatureFlag.SUBSCRIPTION_TIERS_ENABLED,
|
|
||||||
true // Default to true (new design)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
get onboardingSurveyEnabled() {
|
get onboardingSurveyEnabled() {
|
||||||
return (
|
return (
|
||||||
remoteConfig.value.onboarding_survey_enabled ??
|
remoteConfig.value.onboarding_survey_enabled ??
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
|
|
||||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
|
||||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||||
import { isCloud } from '@/platform/distribution/types'
|
import { isCloud } from '@/platform/distribution/types'
|
||||||
import { useTelemetry } from '@/platform/telemetry'
|
import { useTelemetry } from '@/platform/telemetry'
|
||||||
@@ -54,10 +53,7 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const { subscribe, isActiveSubscription, fetchStatus, showSubscriptionDialog } =
|
const { subscribe, isActiveSubscription, fetchStatus, showSubscriptionDialog } =
|
||||||
useSubscription()
|
useSubscription()
|
||||||
const { flags } = useFeatureFlags()
|
|
||||||
const shouldUseStripePricing = computed(
|
|
||||||
() => isCloud && Boolean(flags.subscriptionTiersEnabled)
|
|
||||||
)
|
|
||||||
const telemetry = useTelemetry()
|
const telemetry = useTelemetry()
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
@@ -112,7 +108,7 @@ const stopPolling = () => {
|
|||||||
watch(
|
watch(
|
||||||
[isAwaitingStripeSubscription, isActiveSubscription],
|
[isAwaitingStripeSubscription, isActiveSubscription],
|
||||||
([awaiting, isActive]) => {
|
([awaiting, isActive]) => {
|
||||||
if (shouldUseStripePricing.value && awaiting && isActive) {
|
if (isCloud && awaiting && isActive) {
|
||||||
emit('subscribed')
|
emit('subscribed')
|
||||||
isAwaitingStripeSubscription.value = false
|
isAwaitingStripeSubscription.value = false
|
||||||
}
|
}
|
||||||
@@ -122,9 +118,6 @@ watch(
|
|||||||
const handleSubscribe = async () => {
|
const handleSubscribe = async () => {
|
||||||
if (isCloud) {
|
if (isCloud) {
|
||||||
useTelemetry()?.trackSubscription('subscribe_clicked')
|
useTelemetry()?.trackSubscription('subscribe_clicked')
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldUseStripePricing.value) {
|
|
||||||
isAwaitingStripeSubscription.value = true
|
isAwaitingStripeSubscription.value = true
|
||||||
showSubscriptionDialog()
|
showSubscriptionDialog()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ import { useDialogService } from '@/services/dialogService'
|
|||||||
import type { components, operations } from '@/types/comfyRegistryTypes'
|
import type { components, operations } from '@/types/comfyRegistryTypes'
|
||||||
import { useSubscriptionCancellationWatcher } from './useSubscriptionCancellationWatcher'
|
import { useSubscriptionCancellationWatcher } from './useSubscriptionCancellationWatcher'
|
||||||
|
|
||||||
type CloudSubscriptionCheckoutResponse = {
|
type CloudSubscriptionCheckoutResponse = NonNullable<
|
||||||
checkout_url: string
|
operations['createCloudSubscriptionCheckout']['responses']['201']['content']['application/json']
|
||||||
}
|
>
|
||||||
|
|
||||||
export type CloudSubscriptionStatusResponse = NonNullable<
|
export type CloudSubscriptionStatusResponse = NonNullable<
|
||||||
operations['GetCloudSubscriptionStatus']['responses']['200']['content']['application/json']
|
operations['GetCloudSubscriptionStatus']['responses']['200']['content']['application/json']
|
||||||
|
|||||||
@@ -37,6 +37,5 @@ export type RemoteConfig = {
|
|||||||
model_upload_button_enabled?: boolean
|
model_upload_button_enabled?: boolean
|
||||||
asset_update_options_enabled?: boolean
|
asset_update_options_enabled?: boolean
|
||||||
private_models_enabled?: boolean
|
private_models_enabled?: boolean
|
||||||
subscription_tiers_enabled?: boolean
|
|
||||||
onboarding_survey_enabled?: boolean
|
onboarding_survey_enabled?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { Component } from 'vue'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
|
||||||
import { isCloud } from '@/platform/distribution/types'
|
import { isCloud } from '@/platform/distribution/types'
|
||||||
import type { SettingTreeNode } from '@/platform/settings/settingStore'
|
import type { SettingTreeNode } from '@/platform/settings/settingStore'
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
@@ -36,7 +35,6 @@ export function useSettingUI(
|
|||||||
|
|
||||||
const { shouldRenderVueNodes } = useVueFeatureFlags()
|
const { shouldRenderVueNodes } = useVueFeatureFlags()
|
||||||
const { isActiveSubscription } = useSubscription()
|
const { isActiveSubscription } = useSubscription()
|
||||||
const { flags } = useFeatureFlags()
|
|
||||||
|
|
||||||
const settingRoot = computed<SettingTreeNode>(() => {
|
const settingRoot = computed<SettingTreeNode>(() => {
|
||||||
const root = buildTree(
|
const root = buildTree(
|
||||||
@@ -106,7 +104,6 @@ export function useSettingUI(
|
|||||||
|
|
||||||
const shouldShowPlanCreditsPanel = computed(() => {
|
const shouldShowPlanCreditsPanel = computed(() => {
|
||||||
if (!subscriptionPanel) return false
|
if (!subscriptionPanel) return false
|
||||||
if (!flags.subscriptionTiersEnabled) return true
|
|
||||||
return isActiveSubscription.value
|
return isActiveSubscription.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -40,17 +40,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="isSubgraphNode" class="icon-[comfy--workflow] size-4" />
|
<div v-if="isSubgraphNode" class="icon-[comfy--workflow] size-4" />
|
||||||
<div
|
<div v-if="isApiNode" class="icon-[lucide--component] size-4" />
|
||||||
v-if="isApiNode"
|
|
||||||
:class="
|
|
||||||
cn(
|
|
||||||
'size-4',
|
|
||||||
flags.subscriptionTiersEnabled
|
|
||||||
? 'icon-[lucide--component]'
|
|
||||||
: 'icon-[lucide--dollar-sign]'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Node Title -->
|
<!-- Node Title -->
|
||||||
<div
|
<div
|
||||||
@@ -107,7 +97,6 @@ import EditableText from '@/components/common/EditableText.vue'
|
|||||||
import Button from '@/components/ui/button/Button.vue'
|
import Button from '@/components/ui/button/Button.vue'
|
||||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
|
||||||
import { st } from '@/i18n'
|
import { st } from '@/i18n'
|
||||||
import { LGraphEventMode, RenderShape } from '@/lib/litegraph/src/litegraph'
|
import { LGraphEventMode, RenderShape } from '@/lib/litegraph/src/litegraph'
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
@@ -138,8 +127,6 @@ const emit = defineEmits<{
|
|||||||
'enter-subgraph': []
|
'enter-subgraph': []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { flags } = useFeatureFlags()
|
|
||||||
|
|
||||||
// Error boundary implementation
|
// Error boundary implementation
|
||||||
const renderError = ref<string | null>(null)
|
const renderError = ref<string | null>(null)
|
||||||
const { toastErrorHandler } = useErrorHandling()
|
const { toastErrorHandler } = useErrorHandling()
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { describe, expect, vi } from 'vitest'
|
import { describe, expect, vi } from 'vitest'
|
||||||
|
|
||||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||||
import type { LGraphBadge } from '@/lib/litegraph/src/LGraphBadge'
|
|
||||||
import type { LGraphIcon } from '@/lib/litegraph/src/LGraphIcon'
|
|
||||||
|
|
||||||
import { subgraphTest } from '../../litegraph/subgraph/fixtures/subgraphFixtures'
|
import { subgraphTest } from '../../litegraph/subgraph/fixtures/subgraphFixtures'
|
||||||
|
|
||||||
@@ -17,23 +15,10 @@ vi.mock('@/stores/workspace/colorPaletteStore', () => ({
|
|||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('@/composables/useFeatureFlags', () => ({
|
const { updateSubgraphCredits, getCreditsBadge } = usePriceBadge()
|
||||||
useFeatureFlags: () => ({
|
|
||||||
flags: {
|
|
||||||
subscriptionTiersEnabled: false // Test legacy badge behavior
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
|
|
||||||
const { updateSubgraphCredits } = usePriceBadge()
|
|
||||||
|
|
||||||
const mockNode = new LGraphNode('mock node')
|
const mockNode = new LGraphNode('mock node')
|
||||||
const mockIcon: Partial<LGraphIcon> = { unicode: '\ue96b' }
|
mockNode.badges = [getCreditsBadge('$0.05/Run')]
|
||||||
const badge: Partial<LGraphBadge> = {
|
|
||||||
icon: mockIcon as LGraphIcon,
|
|
||||||
text: '$0.05/Run'
|
|
||||||
}
|
|
||||||
mockNode.badges = [badge as LGraphBadge]
|
|
||||||
|
|
||||||
function getBadgeText(node: LGraphNode): string {
|
function getBadgeText(node: LGraphNode): string {
|
||||||
const badge = node.badges[0]
|
const badge = node.badges[0]
|
||||||
|
|||||||
Reference in New Issue
Block a user