Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/types/RenderContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface RenderUsageData {
extraUsageLimit?: number;
extraUsageUsed?: number;
extraUsageUtilization?: number;
extraUsageCurrency?: string;
error?: 'no-credentials' | 'timeout' | 'rate-limited' | 'api-error' | 'parse-error';
}

Expand Down
6 changes: 4 additions & 2 deletions src/utils/__tests__/usage-fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,8 @@ describe('fetchUsageData error handling', () => {
is_enabled: true,
monthly_limit: 400000,
used_credits: 10600,
utilization: 2.6
utilization: 2.6,
currency: 'EUR'
}
});
const rateLimitedResponseBody = JSON.stringify({
Expand Down Expand Up @@ -572,7 +573,8 @@ describe('fetchUsageData error handling', () => {
extraUsageEnabled: true,
extraUsageLimit: 400000,
extraUsageUsed: 10600,
extraUsageUtilization: 2.6
extraUsageUtilization: 2.6,
extraUsageCurrency: 'EUR'
});
expect(result.second).toEqual(result.first);
expect(result.requestCount).toBe(1);
Expand Down
8 changes: 6 additions & 2 deletions src/utils/usage-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const CachedUsageDataSchema = z.object({
extraUsageLimit: z.number().nullable().optional(),
extraUsageUsed: z.number().nullable().optional(),
extraUsageUtilization: z.number().nullable().optional(),
extraUsageCurrency: z.string().nullable().optional(),
error: z.string().nullable().optional()
});

Expand All @@ -72,7 +73,8 @@ const UsageApiResponseSchema = z.looseObject({
is_enabled: z.boolean().nullable().optional(),
monthly_limit: z.number().nullable().optional(),
used_credits: z.number().nullable().optional(),
utilization: z.number().nullable().optional()
utilization: z.number().nullable().optional(),
currency: z.string().nullable().optional()
}).nullable().optional()
});

Expand Down Expand Up @@ -115,6 +117,7 @@ function parseCachedUsageData(rawJson: string): UsageData | null {
extraUsageLimit: parsed.extraUsageLimit ?? undefined,
extraUsageUsed: parsed.extraUsageUsed ?? undefined,
extraUsageUtilization: parsed.extraUsageUtilization ?? undefined,
extraUsageCurrency: parsed.extraUsageCurrency ?? undefined,
error: parsedError.success ? parsedError.data : undefined
};
}
Expand All @@ -137,7 +140,8 @@ function parseUsageApiResponse(rawJson: string): UsageData | null {
extraUsageEnabled: parsed.extra_usage?.is_enabled ?? undefined,
extraUsageLimit: parsed.extra_usage?.monthly_limit ?? undefined,
extraUsageUsed: parsed.extra_usage?.used_credits ?? undefined,
extraUsageUtilization: parsed.extra_usage?.utilization ?? undefined
extraUsageUtilization: parsed.extra_usage?.utilization ?? undefined,
extraUsageCurrency: parsed.extra_usage?.currency ?? undefined
};
}

Expand Down
6 changes: 4 additions & 2 deletions src/utils/usage-prefetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ const USAGE_DATA_FIELDS: UsageDataField[] = [
'extraUsageEnabled',
'extraUsageLimit',
'extraUsageUsed',
'extraUsageUtilization'
'extraUsageUtilization',
'extraUsageCurrency'
];

interface UsageFieldRequirement {
Expand Down Expand Up @@ -132,7 +133,8 @@ function pickDefinedUsageFields(data: UsageData | null | undefined): Partial<Usa
...(data?.extraUsageEnabled !== undefined ? { extraUsageEnabled: data.extraUsageEnabled } : {}),
...(data?.extraUsageLimit !== undefined ? { extraUsageLimit: data.extraUsageLimit } : {}),
...(data?.extraUsageUsed !== undefined ? { extraUsageUsed: data.extraUsageUsed } : {}),
...(data?.extraUsageUtilization !== undefined ? { extraUsageUtilization: data.extraUsageUtilization } : {})
...(data?.extraUsageUtilization !== undefined ? { extraUsageUtilization: data.extraUsageUtilization } : {}),
...(data?.extraUsageCurrency !== undefined ? { extraUsageCurrency: data.extraUsageCurrency } : {})
};
}

Expand Down
1 change: 1 addition & 0 deletions src/utils/usage-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface UsageData {
extraUsageLimit?: number; // in cents (divide by 100 for dollars)
extraUsageUsed?: number; // in cents (divide by 100 for dollars)
extraUsageUtilization?: number; // percentage 0-100
extraUsageCurrency?: string; // ISO 4217 currency code (e.g. 'USD', 'EUR')
error?: UsageError;
}

Expand Down
3 changes: 2 additions & 1 deletion src/widgets/ExtraUsageRemaining.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
} from '../types/Widget';
import { getUsageErrorMessage } from '../utils/usage';

import { formatUsageCurrency } from './shared/currency';
import {
appendHideDisabledModifier,
getHideExtraUsageDisabledKeybind,
Expand Down Expand Up @@ -54,7 +55,7 @@ export class ExtraUsageRemainingWidget implements Widget {
const limitDollars = data.extraUsageLimit / 100;
const usedDollars = data.extraUsageUsed / 100;
const remaining = Math.max(0, limitDollars - usedDollars);
const formatted = `$${remaining.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
const formatted = formatUsageCurrency(remaining, data.extraUsageCurrency);

return formatRawOrLabeledValue(item, 'Overage Left: ', formatted);
}
Expand Down
13 changes: 13 additions & 0 deletions src/widgets/__tests__/ExtraUsageRemaining.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ describe('ExtraUsageRemainingWidget', () => {
}, context)).toBe('$3,894.00');
});

it('formats remaining budget in the currency reported by the API', () => {
const widget = new ExtraUsageRemainingWidget();

expect(render(widget, { id: 'extra', type: 'extra-usage-remaining' }, {
usageData: {
extraUsageCurrency: 'EUR',
extraUsageEnabled: true,
extraUsageLimit: 400000,
extraUsageUsed: 10600
}
})).toBe('Overage Left: €3,894.00');
});

it('clamps remaining budget at zero', () => {
const widget = new ExtraUsageRemainingWidget();

Expand Down
23 changes: 23 additions & 0 deletions src/widgets/__tests__/shared/currency.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {
describe,
expect,
it
} from 'vitest';

import { formatUsageCurrency } from '../../shared/currency';

describe('formatUsageCurrency', () => {
it('defaults to USD when no currency is reported', () => {
expect(formatUsageCurrency(3894, undefined)).toBe('$3,894.00');
});

it('formats known ISO 4217 currency codes', () => {
expect(formatUsageCurrency(3894, 'USD')).toBe('$3,894.00');
expect(formatUsageCurrency(3894, 'EUR')).toBe('€3,894.00');
expect(formatUsageCurrency(5.42, 'GBP')).toBe('£5.42');
});

it('falls back to USD for invalid currency codes', () => {
expect(formatUsageCurrency(3894, 'not-a-currency')).toBe('$3,894.00');
});
});
23 changes: 23 additions & 0 deletions src/widgets/shared/currency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const FALLBACK_CURRENCY = 'USD';

/**
* Formats a monetary amount using the ISO 4217 currency code reported by the
* usage API (extra_usage.currency), falling back to USD when absent or invalid.
*/
export function formatUsageCurrency(amount: number, currency: string | undefined): string {
try {
return amount.toLocaleString('en-US', {
style: 'currency',
currency: currency ?? FALLBACK_CURRENCY,
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
} catch {
return amount.toLocaleString('en-US', {
style: 'currency',
currency: FALLBACK_CURRENCY,
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
}
}