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
7 changes: 7 additions & 0 deletions apps/mobile/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ Humans: follow instructions below or talk to [@iscekic](https://github.com/iscek
- PostHog feature flag infrastructure and the version-aware registry remain available for future flags.
- KiloClaw tab visibility still depends on instance ownership.

## Profile credits

The bonus-credit subtitle includes only grants with an expiry strictly after the
current render time. Elapsed grants and non-expiring credits do not contribute to
its amount or earliest expiry, for both personal and organization accounts.
The balance continues to come from the balance query.

## Getting started

Generally speaking, you only need a new dev build if making dependency/native changes.
Expand Down
30 changes: 30 additions & 0 deletions apps/mobile/src/components/profile-credits-card.mounted.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,36 @@ beforeEach(() => {
});

describe('CreditsCard balance state', () => {
it.each([
{ name: 'expired grants', future: false },
{ name: 'mixed grants', future: true },
])('excludes elapsed expiry dates from $name', async ({ future }) => {
const now = Date.parse('2026-10-17T12:00:00Z');
const clock = vi.spyOn(Date, 'now').mockReturnValue(now);
currentUser.userId = 'user-a';
getContextBalanceQueryFn.mockResolvedValue({ balance: 0 });
personalCreditBlocksQueryFn.mockResolvedValue({
creditBlocks: [
{ balance_mUsd: 1_900_000, expiry_date: '2026-06-17T12:00:00Z' },
{ balance_mUsd: 3_000_000, expiry_date: '2026-10-17T12:00:00Z' },
{ balance_mUsd: 7_000_000, expiry_date: null },
...(future ? [{ balance_mUsd: 2_000_000, expiry_date: '2026-12-17T12:00:00Z' }] : []),
],
});
try {
const { texts, unmount } = await mountCard();
try {
await waitFor(() => texts().includes('$0.00') && !texts().includes('SKELETON'));
const notes = texts().filter(text => text.includes('in bonus credits'));
expect(notes).toEqual(future ? ['$2.00 in bonus credits expiring 12/17/2026'] : []);
} finally {
unmount();
}
} finally {
clock.mockRestore();
}
});

it.each([
{ orgs: [], options: ['Personal', 'Cancel'], choice: 0, expected: null, label: 'Personal' },
{
Expand Down
6 changes: 5 additions & 1 deletion apps/mobile/src/components/profile-credits-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,11 @@ export function CreditsCard({ enabled, orgs }: Readonly<CreditsCardProps>) {
const balancePending = balance === undefined && !balanceFailed;
const showBalanceSkeleton =
(balanceLoading || balancePending || !hideBalanceLoaded) && !balanceFailed;
const expiringBlocks = creditData?.creditBlocks.filter(b => b.expiry_date !== null) ?? [];
const now = Date.now();
const expiringBlocks =
creditData?.creditBlocks.filter(
b => b.expiry_date !== null && parseTimestamp(b.expiry_date).getTime() > now
) ?? [];
const expiringTotal = fromMicrodollars(
expiringBlocks.reduce((sum, b) => sum + b.balance_mUsd, 0)
);
Expand Down
Loading