mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-24 16:54:03 +00:00
## Summary Fix the issue where local/desktop users would top up and then see the settings homepage dialog instead of the credits tab. ## Changes - **What**: useSubscription.ts, TopUpCreditsDialogContent.vue, SubscriptionRequiredDialogContent.vue - **Breaking**: <!-- Any breaking changes (if none, remove this line) --> - **Dependencies**: <!-- New dependencies (if none, remove this line) --> ## Screenshots (if applicable) Showing this screen after topping up on local/desktop: <img width="789" height="752" alt="image" src="https://github.com/user-attachments/assets/ea92b30b-5c3b-412a-acbe-1b0893621e53" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7990-feat-local-legacy-settings-dialog-fix-2e76d73d365081c7b1e1f96a24745e7b) by [Unito](https://www.unito.io)
278 lines
8.4 KiB
Vue
278 lines
8.4 KiB
Vue
<template>
|
|
<div
|
|
class="flex min-w-[460px] flex-col rounded-2xl border border-border-default bg-base-background shadow-[1px_1px_8px_0_rgba(0,0,0,0.4)]"
|
|
>
|
|
<!-- Header -->
|
|
<div class="flex py-8 items-center justify-between px-8">
|
|
<h2 class="text-lg font-bold text-base-foreground m-0">
|
|
{{
|
|
isInsufficientCredits
|
|
? $t('credits.topUp.addMoreCreditsToRun')
|
|
: $t('credits.topUp.addMoreCredits')
|
|
}}
|
|
</h2>
|
|
<button
|
|
class="cursor-pointer rounded border-none bg-transparent p-0 text-muted-foreground transition-colors hover:text-base-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-secondary-foreground"
|
|
@click="() => handleClose()"
|
|
>
|
|
<i class="icon-[lucide--x] size-6" />
|
|
</button>
|
|
</div>
|
|
<p
|
|
v-if="isInsufficientCredits"
|
|
class="text-sm text-muted-foreground m-0 px-8"
|
|
>
|
|
{{ $t('credits.topUp.insufficientWorkflowMessage') }}
|
|
</p>
|
|
|
|
<!-- Preset amount buttons -->
|
|
<div class="px-8">
|
|
<h3 class="m-0 text-sm font-normal text-muted-foreground">
|
|
{{ $t('credits.topUp.selectAmount') }}
|
|
</h3>
|
|
<div class="flex gap-2 pt-3">
|
|
<Button
|
|
v-for="amount in PRESET_AMOUNTS"
|
|
:key="amount"
|
|
:autofocus="amount === 50"
|
|
variant="secondary"
|
|
size="lg"
|
|
:class="
|
|
cn(
|
|
'h-10 text-base font-medium w-full focus-visible:ring-secondary-foreground',
|
|
selectedPreset === amount && 'bg-secondary-background-selected'
|
|
)
|
|
"
|
|
@click="handlePresetClick(amount)"
|
|
>
|
|
${{ amount }}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<!-- Amount (USD) / Credits -->
|
|
<div class="flex gap-2 px-8 pt-8">
|
|
<!-- You Pay -->
|
|
<div class="flex flex-1 flex-col gap-3">
|
|
<div class="text-sm text-muted-foreground">
|
|
{{ $t('credits.topUp.youPay') }}
|
|
</div>
|
|
<FormattedNumberStepper
|
|
:model-value="payAmount"
|
|
:min="0"
|
|
:max="MAX_AMOUNT"
|
|
:step="getStepAmount"
|
|
@update:model-value="handlePayAmountChange"
|
|
@max-reached="showCeilingWarning = true"
|
|
>
|
|
<template #prefix>
|
|
<span class="shrink-0 text-base font-semibold text-base-foreground"
|
|
>$</span
|
|
>
|
|
</template>
|
|
</FormattedNumberStepper>
|
|
</div>
|
|
|
|
<!-- You Get -->
|
|
<div class="flex flex-1 flex-col gap-3">
|
|
<div class="text-sm text-muted-foreground">
|
|
{{ $t('credits.topUp.youGet') }}
|
|
</div>
|
|
<FormattedNumberStepper
|
|
v-model="creditsModel"
|
|
:min="0"
|
|
:max="usdToCredits(MAX_AMOUNT)"
|
|
:step="getCreditsStepAmount"
|
|
@max-reached="showCeilingWarning = true"
|
|
>
|
|
<template #prefix>
|
|
<i class="icon-[lucide--component] size-4 shrink-0 text-gold-500" />
|
|
</template>
|
|
</FormattedNumberStepper>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Warnings -->
|
|
|
|
<p
|
|
v-if="isBelowMin"
|
|
class="text-sm text-red-500 m-0 px-8 pt-4 text-center flex items-center justify-center gap-1"
|
|
>
|
|
<i class="icon-[lucide--component] size-4" />
|
|
{{
|
|
$t('credits.topUp.minRequired', {
|
|
credits: formatNumber(usdToCredits(MIN_AMOUNT))
|
|
})
|
|
}}
|
|
</p>
|
|
<p
|
|
v-if="showCeilingWarning"
|
|
class="text-sm text-gold-500 m-0 px-8 pt-4 text-center flex items-center justify-center gap-1"
|
|
>
|
|
<i class="icon-[lucide--component] size-4" />
|
|
{{
|
|
$t('credits.topUp.maxAllowed', {
|
|
credits: formatNumber(usdToCredits(MAX_AMOUNT))
|
|
})
|
|
}}
|
|
<span>{{ $t('credits.topUp.needMore') }}</span>
|
|
<a
|
|
href="https://www.comfy.org/cloud/enterprise"
|
|
target="_blank"
|
|
class="ml-1 text-inherit"
|
|
>{{ $t('credits.topUp.contactUs') }}</a
|
|
>
|
|
</p>
|
|
|
|
<div class="pt-8 pb-8 flex flex-col gap-8 px-8">
|
|
<Button
|
|
:disabled="!isValidAmount || loading"
|
|
:loading="loading"
|
|
variant="primary"
|
|
size="lg"
|
|
class="h-10 justify-center"
|
|
@click="handleBuy"
|
|
>
|
|
{{ $t('credits.topUp.buyCredits') }}
|
|
</Button>
|
|
<div class="flex items-center justify-center gap-1">
|
|
<a
|
|
:href="pricingUrl"
|
|
target="_blank"
|
|
class="flex items-center gap-1 text-sm text-muted-foreground no-underline transition-colors hover:text-base-foreground"
|
|
>
|
|
{{ $t('credits.topUp.viewPricing') }}
|
|
<i class="icon-[lucide--external-link] size-4" />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useToast } from 'primevue/usetoast'
|
|
import { computed, ref } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
import { creditsToUsd, usdToCredits } from '@/base/credits/comfyCredits'
|
|
import Button from '@/components/ui/button/Button.vue'
|
|
import FormattedNumberStepper from '@/components/ui/stepper/FormattedNumberStepper.vue'
|
|
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
|
import { useExternalLink } from '@/composables/useExternalLink'
|
|
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
|
import { useTelemetry } from '@/platform/telemetry'
|
|
import { clearTopupTracking } from '@/platform/telemetry/topupTracker'
|
|
import { useDialogService } from '@/services/dialogService'
|
|
import { useDialogStore } from '@/stores/dialogStore'
|
|
import { cn } from '@/utils/tailwindUtil'
|
|
|
|
const { isInsufficientCredits = false } = defineProps<{
|
|
isInsufficientCredits?: boolean
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
const authActions = useFirebaseAuthActions()
|
|
const dialogStore = useDialogStore()
|
|
const dialogService = useDialogService()
|
|
const telemetry = useTelemetry()
|
|
const toast = useToast()
|
|
const { buildDocsUrl, docsPaths } = useExternalLink()
|
|
const { isSubscriptionEnabled } = useSubscription()
|
|
|
|
// Constants
|
|
const PRESET_AMOUNTS = [10, 25, 50, 100]
|
|
const MIN_AMOUNT = 5
|
|
const MAX_AMOUNT = 10000
|
|
|
|
// State
|
|
const selectedPreset = ref<number | null>(50)
|
|
const payAmount = ref(50)
|
|
const showCeilingWarning = ref(false)
|
|
const loading = ref(false)
|
|
|
|
// Computed
|
|
const pricingUrl = computed(() =>
|
|
buildDocsUrl(docsPaths.partnerNodesPricing, { includeLocale: true })
|
|
)
|
|
|
|
const creditsModel = computed({
|
|
get: () => usdToCredits(payAmount.value),
|
|
set: (newCredits: number) => {
|
|
payAmount.value = Math.round(creditsToUsd(newCredits))
|
|
selectedPreset.value = null
|
|
}
|
|
})
|
|
|
|
const isValidAmount = computed(
|
|
() => payAmount.value >= MIN_AMOUNT && payAmount.value <= MAX_AMOUNT
|
|
)
|
|
|
|
const isBelowMin = computed(() => payAmount.value < MIN_AMOUNT)
|
|
|
|
// Utility functions
|
|
function formatNumber(num: number): string {
|
|
return num.toLocaleString('en-US')
|
|
}
|
|
|
|
// Step amount functions
|
|
function getStepAmount(currentAmount: number): number {
|
|
if (currentAmount < 100) return 5
|
|
if (currentAmount < 1000) return 50
|
|
return 100
|
|
}
|
|
|
|
function getCreditsStepAmount(currentCredits: number): number {
|
|
const usdAmount = creditsToUsd(currentCredits)
|
|
return usdToCredits(getStepAmount(usdAmount))
|
|
}
|
|
|
|
// Event handlers
|
|
function handlePayAmountChange(value: number) {
|
|
payAmount.value = value
|
|
selectedPreset.value = null
|
|
showCeilingWarning.value = false
|
|
}
|
|
|
|
function handlePresetClick(amount: number) {
|
|
showCeilingWarning.value = false
|
|
payAmount.value = amount
|
|
selectedPreset.value = amount
|
|
}
|
|
|
|
function handleClose(clearTracking = true) {
|
|
if (clearTracking) {
|
|
clearTopupTracking()
|
|
}
|
|
dialogStore.closeDialog({ key: 'top-up-credits' })
|
|
}
|
|
|
|
async function handleBuy() {
|
|
// Prevent double-clicks
|
|
if (loading.value || !isValidAmount.value) return
|
|
|
|
loading.value = true
|
|
try {
|
|
telemetry?.trackApiCreditTopupButtonPurchaseClicked(payAmount.value)
|
|
await authActions.purchaseCredits(payAmount.value)
|
|
|
|
// Close top-up dialog (keep tracking) and open credits panel to show updated balance
|
|
handleClose(false)
|
|
dialogService.showSettingsDialog(
|
|
isSubscriptionEnabled() ? 'subscription' : 'credits'
|
|
)
|
|
} catch (error) {
|
|
console.error('Purchase failed:', error)
|
|
|
|
const errorMessage =
|
|
error instanceof Error ? error.message : t('credits.topUp.unknownError')
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: t('credits.topUp.purchaseError'),
|
|
detail: t('credits.topUp.purchaseErrorDetail', { error: errorMessage }),
|
|
life: 5000
|
|
})
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
</script>
|