From aef40834f308e8864e4457d4bd03d8e3afcc5aff Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Tue, 9 Dec 2025 04:11:27 -0800 Subject: [PATCH] add shared comfy credit conversion helpers (#7061) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/base/credits/comfyCredits.ts | 125 +++++++++++++++ src/components/common/UserCredit.vue | 13 +- .../content/TopUpCreditsDialogContent.vue | 150 +++++++++++++++++- .../content/credit/CreditTopUpOption.vue | 98 ++++-------- .../credit/LegacyCreditTopUpOption.vue | 119 ++++++++++++++ src/composables/useFeatureFlags.ts | 2 +- src/locales/en/main.json | 15 +- .../components/SubscriptionPanel.vue | 6 +- .../composables/useSubscriptionCredits.ts | 65 +++----- .../tests/base/credits/comfyCredits.test.ts | 46 ++++++ .../content/credit/CreditTopUpOption.test.ts | 52 ++++++ .../composables/node/useCreditsBadge.test.ts | 8 + .../components/SubscriptionPanel.test.ts | 10 +- .../useSubscriptionCredits.test.ts | 78 +++++---- 14 files changed, 628 insertions(+), 159 deletions(-) create mode 100644 src/base/credits/comfyCredits.ts create mode 100644 src/components/dialog/content/credit/LegacyCreditTopUpOption.vue create mode 100644 tests-ui/tests/base/credits/comfyCredits.test.ts create mode 100644 tests-ui/tests/components/dialog/content/credit/CreditTopUpOption.test.ts diff --git a/src/base/credits/comfyCredits.ts b/src/base/credits/comfyCredits.ts new file mode 100644 index 000000000..b49df573f --- /dev/null +++ b/src/base/credits/comfyCredits.ts @@ -0,0 +1,125 @@ +const DEFAULT_NUMBER_FORMAT: Intl.NumberFormatOptions = { + minimumFractionDigits: 2, + maximumFractionDigits: 2 +} + +const formatNumber = ({ + value, + locale, + options +}: { + value: number + locale?: string + options?: Intl.NumberFormatOptions +}): string => { + const merged: Intl.NumberFormatOptions = { + ...DEFAULT_NUMBER_FORMAT, + ...options + } + + if ( + typeof merged.maximumFractionDigits === 'number' && + typeof merged.minimumFractionDigits === 'number' && + merged.maximumFractionDigits < merged.minimumFractionDigits + ) { + merged.minimumFractionDigits = merged.maximumFractionDigits + } + + return new Intl.NumberFormat(locale, merged).format(value) +} + +export const CREDITS_PER_USD = 211 +export const COMFY_CREDIT_RATE_CENTS = CREDITS_PER_USD / 100 // credits per cent + +export const usdToCents = (usd: number): number => Math.round(usd * 100) + +export const centsToCredits = (cents: number): number => + Math.round(cents * COMFY_CREDIT_RATE_CENTS) + +export const creditsToCents = (credits: number): number => + Math.round(credits / COMFY_CREDIT_RATE_CENTS) + +export const usdToCredits = (usd: number): number => + Math.round(usd * CREDITS_PER_USD) + +export const creditsToUsd = (credits: number): number => + Math.round((credits / CREDITS_PER_USD) * 100) / 100 + +export type FormatOptions = { + value: number + locale?: string + numberOptions?: Intl.NumberFormatOptions +} + +export type FormatFromCentsOptions = { + cents: number + locale?: string + numberOptions?: Intl.NumberFormatOptions +} + +export type FormatFromUsdOptions = { + usd: number + locale?: string + numberOptions?: Intl.NumberFormatOptions +} + +export const formatCredits = ({ + value, + locale, + numberOptions +}: FormatOptions): string => + formatNumber({ value, locale, options: numberOptions }) + +export const formatCreditsFromCents = ({ + cents, + locale, + numberOptions +}: FormatFromCentsOptions): string => + formatCredits({ + value: centsToCredits(cents), + locale, + numberOptions + }) + +export const formatCreditsFromUsd = ({ + usd, + locale, + numberOptions +}: FormatFromUsdOptions): string => + formatCredits({ + value: usdToCredits(usd), + locale, + numberOptions + }) + +export const formatUsd = ({ + value, + locale, + numberOptions +}: FormatOptions): string => + formatNumber({ + value, + locale, + options: numberOptions + }) + +export const formatUsdFromCents = ({ + cents, + locale, + numberOptions +}: FormatFromCentsOptions): string => + formatUsd({ + value: cents / 100, + locale, + numberOptions + }) + +/** + * Clamps a USD value to the allowed range for credit purchases + * @param value - The USD amount to clamp + * @returns The clamped value between $1 and $1000, or 0 if NaN + */ +export const clampUsd = (value: number): number => { + if (Number.isNaN(value)) return 0 + return Math.min(1000, Math.max(1, value)) +} diff --git a/src/components/common/UserCredit.vue b/src/components/common/UserCredit.vue index 15a7eb404..e3032fc4f 100644 --- a/src/components/common/UserCredit.vue +++ b/src/components/common/UserCredit.vue @@ -26,10 +26,11 @@ import Skeleton from 'primevue/skeleton' import Tag from 'primevue/tag' import { computed } from 'vue' +import { useI18n } from 'vue-i18n' +import { formatCreditsFromCents } from '@/base/credits/comfyCredits' import { useFeatureFlags } from '@/composables/useFeatureFlags' import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' -import { formatMetronomeCurrency } from '@/utils/formatUtil' const { textClass } = defineProps<{ textClass?: string @@ -38,9 +39,15 @@ const { textClass } = defineProps<{ const authStore = useFirebaseAuthStore() const { flags } = useFeatureFlags() const balanceLoading = computed(() => authStore.isFetchingBalance) +const { t, locale } = useI18n() const formattedBalance = computed(() => { - if (!authStore.balance) return '0.00' - return formatMetronomeCurrency(authStore.balance.amount_micros, 'usd') + // Backend returns cents despite the *_micros naming convention. + const cents = authStore.balance?.amount_micros ?? 0 + const amount = formatCreditsFromCents({ + cents, + locale: locale.value + }) + return `${amount} ${t('credits.credits')}` }) diff --git a/src/components/dialog/content/TopUpCreditsDialogContent.vue b/src/components/dialog/content/TopUpCreditsDialogContent.vue index 8f1cdd797..12ef94ca0 100644 --- a/src/components/dialog/content/TopUpCreditsDialogContent.vue +++ b/src/components/dialog/content/TopUpCreditsDialogContent.vue @@ -1,5 +1,65 @@