Skip to content

Commit 7c43bf4

Browse files
feat: add comfy credit domain helpers
1 parent c21ded6 commit 7c43bf4

File tree

8 files changed

+283
-268
lines changed

8 files changed

+283
-268
lines changed

src/base/credits/comfyCredits.ts

Lines changed: 80 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
1-
/**
2-
* Fixed conversion rate between USD and Comfy credits.
3-
* 1 credit costs 210 cents ($2.10).
4-
*/
5-
export const COMFY_CREDIT_RATE_CENTS = 210
6-
7-
export const COMFY_CREDIT_RATE_USD = COMFY_CREDIT_RATE_CENTS / 100
8-
91
const DEFAULT_NUMBER_FORMAT: Intl.NumberFormatOptions = {
102
minimumFractionDigits: 2,
113
maximumFractionDigits: 2
124
}
135

14-
const formatNumber = (
15-
value: number,
16-
options: Intl.NumberFormatOptions = DEFAULT_NUMBER_FORMAT,
6+
const formatNumber = ({
7+
value,
8+
locale,
9+
options
10+
}: {
11+
value: number
1712
locale?: string
18-
) => {
13+
options?: Intl.NumberFormatOptions
14+
}): string => {
1915
const merged: Intl.NumberFormatOptions = {
2016
...DEFAULT_NUMBER_FORMAT,
2117
...options
@@ -32,166 +28,88 @@ const formatNumber = (
3228
return new Intl.NumberFormat(locale, merged).format(value)
3329
}
3430

35-
export const centsToUsd = (cents: number): number => cents / 100
31+
export const COMFY_CREDIT_RATE_CENTS = 210
32+
export const COMFY_CREDIT_RATE_USD = COMFY_CREDIT_RATE_CENTS / 100
33+
3634
export const usdToCents = (usd: number): number => Math.round(usd * 100)
3735

38-
/**
39-
* Converts a USD amount into Comfy credits.
40-
*/
41-
export function usdToComfyCredits(usd: number): number {
42-
return usd / COMFY_CREDIT_RATE_USD
43-
}
36+
export const centsToCredits = (cents: number): number =>
37+
cents / COMFY_CREDIT_RATE_CENTS
4438

45-
/**
46-
* Converts USD cents into Comfy credits.
47-
*/
48-
export function centsToComfyCredits(cents: number): number {
49-
return cents / COMFY_CREDIT_RATE_CENTS
50-
}
39+
export const creditsToCents = (credits: number): number =>
40+
Math.round(credits * COMFY_CREDIT_RATE_CENTS)
5141

52-
/**
53-
* Converts Comfy credits back to USD.
54-
*/
55-
export function comfyCreditsToUsd(credits: number): number {
56-
return credits * COMFY_CREDIT_RATE_USD
57-
}
42+
export const usdToCredits = (usd: number): number =>
43+
centsToCredits(usdToCents(usd))
5844

59-
/**
60-
* Converts Comfy credits to cents.
61-
*/
62-
export function comfyCreditsToCents(credits: number): number {
63-
return credits * COMFY_CREDIT_RATE_CENTS
64-
}
45+
export const creditsToUsd = (credits: number): number =>
46+
creditsToCents(credits) / 100
6547

66-
export function formatUsdFromCents(
67-
cents: number,
68-
options?: Intl.NumberFormatOptions,
48+
export type FormatOptions = {
49+
value: number
6950
locale?: string
70-
): string {
71-
return formatNumber(
72-
centsToUsd(cents),
73-
{ ...DEFAULT_NUMBER_FORMAT, ...options },
74-
locale
75-
)
51+
numberOptions?: Intl.NumberFormatOptions
7652
}
7753

78-
/**
79-
* Formats credits to a localized numeric string (no unit suffix).
80-
*/
81-
export function formatComfyCreditsAmount(
82-
credits: number,
83-
options?: Intl.NumberFormatOptions,
54+
export const formatCredits = ({
55+
value,
56+
locale,
57+
numberOptions
58+
}: FormatOptions): string =>
59+
formatNumber({ value, locale, options: numberOptions })
60+
61+
export const formatCreditsFromCents = ({
62+
cents,
63+
locale,
64+
numberOptions
65+
}: {
66+
cents: number
8467
locale?: string
85-
): string {
86-
return formatNumber(credits, { ...DEFAULT_NUMBER_FORMAT, ...options }, locale)
87-
}
88-
89-
type FormatCreditsOptions = {
90-
unit?: string | null
9168
numberOptions?: Intl.NumberFormatOptions
69+
}): string =>
70+
formatCredits({
71+
value: centsToCredits(cents),
72+
locale,
73+
numberOptions
74+
})
75+
76+
export const formatCreditsFromUsd = ({
77+
usd,
78+
locale,
79+
numberOptions
80+
}: {
81+
usd: number
9282
locale?: string
93-
}
94-
95-
export function formatComfyCreditsLabel(
96-
credits: number,
97-
{ unit = 'credits', numberOptions, locale }: FormatCreditsOptions = {}
98-
): string {
99-
const formatted = formatComfyCreditsAmount(credits, numberOptions, locale)
100-
return unit ? `${formatted} ${unit}` : formatted
101-
}
102-
103-
export function formatComfyCreditsLabelFromCents(
104-
cents: number,
105-
options?: FormatCreditsOptions
106-
): string {
107-
return formatComfyCreditsLabel(centsToComfyCredits(cents), options)
108-
}
109-
110-
export function formatComfyCreditsLabelFromUsd(
111-
usd: number,
112-
options?: FormatCreditsOptions
113-
): string {
114-
return formatComfyCreditsLabel(usdToComfyCredits(usd), options)
115-
}
116-
117-
export function formatComfyCreditsRangeLabelFromUsd(
118-
minUsd: number,
119-
maxUsd: number,
120-
{
121-
unit = 'credits',
122-
numberOptions,
83+
numberOptions?: Intl.NumberFormatOptions
84+
}): string =>
85+
formatCredits({
86+
value: usdToCredits(usd),
12387
locale,
124-
separator = '–'
125-
}: FormatCreditsOptions & {
126-
separator?: string
127-
} = {}
128-
): string {
129-
const min = formatComfyCreditsAmount(
130-
usdToComfyCredits(minUsd),
131-
numberOptions,
132-
locale
133-
)
134-
const max = formatComfyCreditsAmount(
135-
usdToComfyCredits(maxUsd),
136-
numberOptions,
137-
locale
138-
)
139-
const joined = `${min}${separator}${max}`
140-
return unit ? `${joined} ${unit}` : joined
141-
}
142-
143-
const USD_RANGE_REGEX = /(~?)\$(\d+(?:\.\d+)?)\s*[-]\s*\$?(\d+(?:\.\d+)?)/g
144-
const USD_VALUE_REGEX = /(~?)\$(\d+(?:\.\d+)?)/g
145-
146-
/**
147-
* Converts a USD-denoted string (e.g., "$0.45-1.2/Run") into a credits string.
148-
* Any "$X" occurrences become "Y credits". Ranges are rendered as "Y–Z credits".
149-
*/
150-
export function convertUsdLabelToCredits(
151-
label: string,
152-
options?: FormatCreditsOptions
153-
): string {
154-
if (!label) return label
155-
const unit = options?.unit ?? 'credits'
156-
const numberOptions = options?.numberOptions
157-
const locale = options?.locale
158-
159-
const formatSingle = (usd: number) =>
160-
formatComfyCreditsLabel(usdToComfyCredits(usd), {
161-
unit,
162-
numberOptions,
163-
locale
164-
})
165-
166-
const formatRange = (min: number, max: number, prefix = '') => {
167-
const minStr = formatComfyCreditsAmount(
168-
usdToComfyCredits(min),
169-
numberOptions,
170-
locale
171-
)
172-
const maxStr = formatComfyCreditsAmount(
173-
usdToComfyCredits(max),
174-
numberOptions,
175-
locale
176-
)
177-
const joined = `${minStr}${maxStr}`
178-
return unit ? `${prefix}${joined} ${unit}` : `${prefix}${joined}`
179-
}
180-
181-
let converted = label
182-
converted = converted.replace(
183-
USD_RANGE_REGEX,
184-
(_match, prefix = '', minUsd, maxUsd) =>
185-
formatRange(parseFloat(minUsd), parseFloat(maxUsd), prefix)
186-
)
187-
188-
converted = converted.replace(
189-
USD_VALUE_REGEX,
190-
(_match, prefix = '', amount) => {
191-
const formatted = formatSingle(parseFloat(amount))
192-
return `${prefix}${formatted}`
193-
}
194-
)
195-
196-
return converted
197-
}
88+
numberOptions
89+
})
90+
91+
export const formatUsd = ({
92+
value,
93+
locale,
94+
numberOptions
95+
}: FormatOptions): string =>
96+
formatNumber({
97+
value,
98+
locale,
99+
options: numberOptions
100+
})
101+
102+
export const formatUsdFromCents = ({
103+
cents,
104+
locale,
105+
numberOptions
106+
}: {
107+
cents: number
108+
locale?: string
109+
numberOptions?: Intl.NumberFormatOptions
110+
}): string =>
111+
formatUsd({
112+
value: cents / 100,
113+
locale,
114+
numberOptions
115+
})

src/components/common/UserCredit.vue

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<div v-else class="flex items-center gap-1">
1010
<Tag
1111
severity="secondary"
12-
icon="pi pi-dollar"
12+
icon="pi pi-wallet"
1313
rounded
1414
class="p-1 text-amber-400"
1515
/>
@@ -21,19 +21,23 @@
2121
import Skeleton from 'primevue/skeleton'
2222
import Tag from 'primevue/tag'
2323
import { computed } from 'vue'
24+
import { useI18n } from 'vue-i18n'
2425
26+
import { formatCreditsFromCents } from '@/base/credits/comfyCredits'
2527
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
26-
import { formatMetronomeCurrency } from '@/utils/formatUtil'
2728
2829
const { textClass } = defineProps<{
2930
textClass?: string
3031
}>()
3132
3233
const authStore = useFirebaseAuthStore()
3334
const balanceLoading = computed(() => authStore.isFetchingBalance)
35+
const { t } = useI18n()
3436
3537
const formattedBalance = computed(() => {
36-
if (!authStore.balance) return '0.00'
37-
return formatMetronomeCurrency(authStore.balance.amount_micros, 'usd')
38+
// Backend returns cents despite the *_micros naming convention.
39+
const cents = authStore.balance?.amount_micros ?? 0
40+
const amount = formatCreditsFromCents({ cents })
41+
return `${amount} ${t('credits.credits')}`
3842
})
3943
</script>

src/components/dialog/content/credit/CreditTopUpOption.vue

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,36 @@
22
<div class="flex items-center gap-2">
33
<Tag
44
severity="secondary"
5-
icon="pi pi-dollar"
5+
icon="pi pi-wallet"
66
rounded
77
class="p-1 text-amber-400"
88
/>
9-
<InputNumber
10-
v-if="editable"
11-
v-model="customAmount"
12-
:min="1"
13-
:max="1000"
14-
:step="1"
15-
show-buttons
16-
:allow-empty="false"
17-
:highlight-on-focus="true"
18-
pt:pc-input-text:root="w-24"
19-
@blur="(e: InputNumberBlurEvent) => (customAmount = Number(e.value))"
20-
@input="(e: InputNumberInputEvent) => (customAmount = Number(e.value))"
21-
/>
22-
<span v-else class="text-xl">{{ amount }}</span>
9+
<div v-if="editable" class="flex items-center gap-2">
10+
<InputNumber
11+
v-model="customAmount"
12+
:min="1"
13+
:max="1000"
14+
:step="1"
15+
show-buttons
16+
:allow-empty="false"
17+
:highlight-on-focus="true"
18+
prefix="$"
19+
pt:pc-input-text:root="w-28"
20+
@blur="
21+
(e: InputNumberBlurEvent) =>
22+
(customAmount = clampUsd(Number(e.value)))
23+
"
24+
@input="
25+
(e: InputNumberInputEvent) =>
26+
(customAmount = clampUsd(Number(e.value)))
27+
"
28+
/>
29+
<span class="text-xs text-muted">{{ formattedCredits }}</span>
30+
</div>
31+
<div v-else class="flex flex-col leading-tight">
32+
<span class="text-xl font-semibold">{{ formattedCredits }}</span>
33+
<span class="text-xs text-muted">{{ formattedUsd }}</span>
34+
</div>
2335
</div>
2436
<ProgressSpinner v-if="loading" class="h-8 w-8" />
2537
<Button
@@ -40,8 +52,10 @@ import type {
4052
} from 'primevue/inputnumber'
4153
import ProgressSpinner from 'primevue/progressspinner'
4254
import Tag from 'primevue/tag'
43-
import { onBeforeUnmount, ref } from 'vue'
55+
import { computed, onBeforeUnmount, ref } from 'vue'
56+
import { useI18n } from 'vue-i18n'
4457
58+
import { formatCreditsFromUsd, formatUsd } from '@/base/credits/comfyCredits'
4559
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
4660
import { useTelemetry } from '@/platform/telemetry'
4761
@@ -61,9 +75,28 @@ const {
6175
const customAmount = ref(amount)
6276
const didClickBuyNow = ref(false)
6377
const loading = ref(false)
78+
const { t } = useI18n()
79+
80+
const clampUsd = (value: number) => {
81+
const safe = Number.isNaN(value) ? 0 : value
82+
return Math.min(1000, Math.max(1, safe))
83+
}
84+
85+
const displayUsdAmount = computed(() =>
86+
editable ? clampUsd(Number(customAmount.value)) : clampUsd(amount)
87+
)
88+
89+
const formattedCredits = computed(
90+
() =>
91+
`${formatCreditsFromUsd({ usd: displayUsdAmount.value })} ${t('credits.credits')}`
92+
)
93+
94+
const formattedUsd = computed(
95+
() => `$${formatUsd({ value: displayUsdAmount.value })}`
96+
)
6497
6598
const handleBuyNow = async () => {
66-
const creditAmount = editable ? customAmount.value : amount
99+
const creditAmount = displayUsdAmount.value
67100
telemetry?.trackApiCreditTopupButtonPurchaseClicked(creditAmount)
68101
69102
loading.value = true

0 commit comments

Comments
 (0)