add shared comfy credit conversion helpers (#7061)

Introduces cents<->usd<->credit converters plus basic formatters and
adds test. Lays groundwork to start converting UI components into
displaying comfy credits.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7061-add-shared-comfy-credit-conversion-helpers-2bb6d73d3650810bb34fdf9bb3fc115b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Christian Byrne
2025-12-09 04:11:27 -08:00
committed by GitHub
parent 8209f5a108
commit aef40834f3
14 changed files with 628 additions and 159 deletions

View File

@@ -1,5 +1,65 @@
<template>
<div class="flex w-96 flex-col gap-10 p-2">
<!-- New Credits Design (default) -->
<div
v-if="useNewDesign"
class="flex w-96 flex-col gap-8 p-8 bg-node-component-surface rounded-2xl border border-border-primary"
>
<!-- Header -->
<div class="flex flex-col gap-4">
<h1 class="text-2xl font-semibold text-foreground-primary m-0">
{{ $t('credits.topUp.addMoreCredits') }}
</h1>
<p class="text-sm text-foreground-secondary m-0">
{{ $t('credits.topUp.creditsDescription') }}
</p>
</div>
<!-- Current Balance Section -->
<div class="flex flex-col gap-4">
<div class="flex items-baseline gap-2">
<UserCredit text-class="text-3xl font-bold" />
<span class="text-sm text-foreground-secondary">{{
$t('credits.creditsAvailable')
}}</span>
</div>
<div v-if="refreshDate" class="text-sm text-foreground-secondary">
{{ $t('credits.refreshes', { date: refreshDate }) }}
</div>
</div>
<!-- Credit Options Section -->
<div class="flex flex-col gap-4">
<span class="text-sm text-foreground-secondary">
{{ $t('credits.topUp.howManyCredits') }}
</span>
<div class="flex flex-col gap-2">
<CreditTopUpOption
v-for="option in creditOptions"
:key="option.credits"
:credits="option.credits"
:description="option.description"
:selected="selectedCredits === option.credits"
@select="selectedCredits = option.credits"
/>
</div>
<div class="text-xs text-foreground-secondary">
{{ $t('credits.topUp.templateNote') }}
</div>
</div>
<!-- Buy Button -->
<Button
:disabled="!selectedCredits || loading"
:loading="loading"
severity="primary"
:label="$t('credits.topUp.buy')"
class="w-full"
@click="handleBuy"
/>
</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') }}
@@ -34,14 +94,14 @@
>{{ $t('credits.topUp.quickPurchase') }}:</span
>
<div class="grid grid-cols-[2fr_1fr] gap-2">
<CreditTopUpOption
<LegacyCreditTopUpOption
v-for="amount in amountOptions"
:key="amount"
:amount="amount"
:preselected="amount === preselectedAmountOption"
/>
<CreditTopUpOption :amount="100" :preselected="false" editable />
<LegacyCreditTopUpOption :amount="100" :preselected="false" editable />
</div>
</div>
</div>
@@ -49,23 +109,107 @@
<script setup lang="ts">
import Button from 'primevue/button'
import { useToast } from 'primevue/usetoast'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import {
creditsToUsd,
formatCredits,
formatUsd
} from '@/base/credits/comfyCredits'
import UserCredit from '@/components/common/UserCredit.vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useTelemetry } from '@/platform/telemetry'
import CreditTopUpOption from './credit/CreditTopUpOption.vue'
import LegacyCreditTopUpOption from './credit/LegacyCreditTopUpOption.vue'
interface CreditOption {
credits: number
description: string
}
const {
refreshDate,
isInsufficientCredits = false,
amountOptions = [5, 10, 20, 50],
preselectedAmountOption = 10
} = defineProps<{
refreshDate?: string
isInsufficientCredits?: boolean
amountOptions?: number[]
preselectedAmountOption?: number
}>()
const { flags } = useFeatureFlags()
// Use feature flag to determine design - defaults to true (new design)
const useNewDesign = computed(() => flags.subscriptionTiersEnabled)
const { t, locale } = useI18n()
const authActions = useFirebaseAuthActions()
const telemetry = useTelemetry()
const toast = useToast()
const selectedCredits = ref<number | null>(null)
const loading = ref(false)
const creditOptions: CreditOption[] = [
{
credits: 1000,
description: t('credits.topUp.videosEstimate', { count: 100 })
},
{
credits: 5000,
description: t('credits.topUp.videosEstimate', { count: 500 })
},
{
credits: 10000,
description: t('credits.topUp.videosEstimate', { count: 1000 })
},
{
credits: 20000,
description: t('credits.topUp.videosEstimate', { count: 2000 })
}
]
const handleBuy = async () => {
if (!selectedCredits.value) return
loading.value = true
try {
const usdAmount = creditsToUsd(selectedCredits.value)
telemetry?.trackApiCreditTopupButtonPurchaseClicked(usdAmount)
await authActions.purchaseCredits(usdAmount)
toast.add({
severity: 'success',
summary: t('credits.topUp.purchaseSuccess'),
detail: t('credits.topUp.purchaseSuccessDetail', {
credits: formatCredits({
value: selectedCredits.value,
locale: locale.value
}),
amount: `$${formatUsd({ value: usdAmount, locale: locale.value })}`
}),
life: 3000
})
} 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
}
}
const handleSeeDetails = async () => {
await authActions.accessBillingPortal()