Skip to content

Commit c551cba

Browse files
jurgenwerkclaude
andcommitted
Hide "topped up" message for fresh users who haven't used credits yet
When a new user signs up, they receive an initial daily credit grant. Previously, the profile popover would immediately show "We topped up your account to 2,000 credits since you were getting low", which is confusing for users who haven't spent any credits yet. Added a `dailyCreditGrantCount` field to the user endpoint that counts how many daily_credit ledger entries exist. The "topped up" message is now only shown when the count is greater than 1, meaning the daily cron has actually topped up the user after they spent credits. Fresh users with only the initial signup grant (count=1) will only see the "Last daily credits grant" line. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 019de3d commit c551cba

6 files changed

Lines changed: 93 additions & 14 deletions

File tree

packages/billing/billing-queries.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -336,25 +336,35 @@ export async function sumUpCreditsLedger(
336336
return results[0].sum === null ? 0 : parseInt(results[0].sum as string);
337337
}
338338

339-
export async function getLastDailyCreditGrantAt(
339+
export async function getDailyCreditGrantInfo(
340340
dbAdapter: DBAdapter,
341341
userId: string,
342-
): Promise<number | null> {
342+
): Promise<{
343+
lastDailyCreditGrantAt: number | null;
344+
dailyCreditGrantCount: number;
345+
}> {
343346
let results = await query(dbAdapter, [
344-
`SELECT MAX(created_at) AS last_grant_at FROM credits_ledger WHERE user_id = `,
347+
`SELECT MAX(created_at) AS last_grant_at, COUNT(*) AS grant_count FROM credits_ledger WHERE user_id = `,
345348
param(userId),
346349
` AND credit_type = 'daily_credit'`,
347350
]);
348351

349352
let lastGrantAt = results[0]?.last_grant_at;
350-
if (lastGrantAt == null) {
351-
return null;
353+
let parsed: number | null = null;
354+
if (lastGrantAt != null) {
355+
parsed =
356+
typeof lastGrantAt === 'number'
357+
? lastGrantAt
358+
: parseInt(lastGrantAt as string);
359+
if (Number.isNaN(parsed)) {
360+
parsed = null;
361+
}
352362
}
353-
let parsed =
354-
typeof lastGrantAt === 'number'
355-
? lastGrantAt
356-
: parseInt(lastGrantAt as string);
357-
return Number.isNaN(parsed) ? null : parsed;
363+
364+
return {
365+
lastDailyCreditGrantAt: parsed,
366+
dailyCreditGrantCount: parseInt(results[0]?.grant_count as string) || 0,
367+
};
358368
}
359369

360370
export async function getCurrentActiveSubscription(

packages/host/app/components/with-subscription-data.gts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ export default class WithSubscriptionData extends Component<WithSubscriptionData
124124
return this.billingService.subscriptionData?.nextDailyCreditGrantAt;
125125
}
126126

127+
private get dailyCreditGrantCount() {
128+
return this.billingService.subscriptionData?.dailyCreditGrantCount ?? 0;
129+
}
130+
127131
private get monthlyCreditText() {
128132
return this.creditsAvailableInPlanAllowance != null &&
129133
this.creditsIncludedInPlanAllowance != null
@@ -191,8 +195,14 @@ export default class WithSubscriptionData extends Component<WithSubscriptionData
191195
let thresholdAmount = formatNumber(this.lowCreditThreshold, {
192196
size: 'short',
193197
});
198+
// Only append "since you were getting low" when the daily cron has
199+
// topped the user up after they actually spent credits (count > 1).
200+
let toppedUpMessage =
201+
this.dailyCreditGrantCount > 1
202+
? `We topped up your account to ${thresholdAmount} credits since you were getting low.`
203+
: `We topped up your account to ${thresholdAmount} credits.`;
194204
return [
195-
`We topped up your account to ${thresholdAmount} credits since you were getting low.`,
205+
toppedUpMessage,
196206
`Last daily credits grant: ${distance} (${timestampLastDailyCreditGrant})`,
197207
];
198208
}

packages/host/app/services/billing-service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ interface SubscriptionData {
2525
lowCreditThreshold: number | null;
2626
lastDailyCreditGrantAt: number | null;
2727
nextDailyCreditGrantAt: number | null;
28+
dailyCreditGrantCount: number;
2829
}
2930

3031
export default class BillingService extends Service {
@@ -155,6 +156,8 @@ export default class BillingService extends Service {
155156
json.data?.attributes?.lastDailyCreditGrantAt ?? null;
156157
let nextDailyCreditGrantAt =
157158
json.data?.attributes?.nextDailyCreditGrantAt ?? null;
159+
let dailyCreditGrantCount =
160+
json.data?.attributes?.dailyCreditGrantCount ?? 0;
158161

159162
this._subscriptionData = {
160163
plan,
@@ -166,6 +169,7 @@ export default class BillingService extends Service {
166169
lowCreditThreshold,
167170
lastDailyCreditGrantAt,
168171
nextDailyCreditGrantAt,
172+
dailyCreditGrantCount,
169173
};
170174
} finally {
171175
this._loadingSubscriptionData = false;

packages/host/tests/acceptance/operator-mode-acceptance-test.gts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,6 +962,7 @@ module('Acceptance | operator mode tests', function (hooks) {
962962
nextDailyCreditGrantAt?: number | null;
963963
lastDailyCreditGrantAt?: number | null;
964964
stripeCustomerEmail?: string | null;
965+
dailyCreditGrantCount?: number;
965966
};
966967

967968
type UserResponseBody = {
@@ -1414,6 +1415,7 @@ module('Acceptance | operator mode tests', function (hooks) {
14141415
lowCreditThreshold: 2000,
14151416
nextDailyCreditGrantAt: null,
14161417
lastDailyCreditGrantAt: nowSeconds - 3600,
1418+
dailyCreditGrantCount: 2,
14171419
};
14181420
try {
14191421
await visitOperatorMode({
@@ -1438,6 +1440,43 @@ module('Acceptance | operator mode tests', function (hooks) {
14381440
}
14391441
});
14401442

1443+
test('does not show "getting low" for fresh users with only initial grant', async function (assert) {
1444+
let originalAttributes = { ...userResponseBody.data.attributes };
1445+
let nowSeconds = Math.floor(Date.now() / 1000);
1446+
userResponseBody.data.attributes = {
1447+
...originalAttributes,
1448+
creditsAvailableInPlanAllowance: 2000,
1449+
creditsIncludedInPlanAllowance: 2000,
1450+
extraCreditsAvailableInBalance: 2000,
1451+
lowCreditThreshold: 2000,
1452+
nextDailyCreditGrantAt: null,
1453+
lastDailyCreditGrantAt: nowSeconds - 60,
1454+
dailyCreditGrantCount: 1,
1455+
};
1456+
try {
1457+
await visitOperatorMode({
1458+
submode: 'interact',
1459+
codePath: `${testRealmURL}employee.gts`,
1460+
});
1461+
1462+
await waitFor('[data-test-profile-icon-button]');
1463+
await click('[data-test-profile-icon-button]');
1464+
1465+
assert.dom('[data-test-daily-grant-note]').exists();
1466+
assert
1467+
.dom('[data-test-daily-grant-note]')
1468+
.includesText('We topped up your account to 2,000 credits.');
1469+
assert
1470+
.dom('[data-test-daily-grant-note]')
1471+
.doesNotIncludeText('getting low');
1472+
assert
1473+
.dom('[data-test-daily-grant-note]')
1474+
.includesText('Last daily credits grant:');
1475+
} finally {
1476+
userResponseBody.data.attributes = originalAttributes;
1477+
}
1478+
});
1479+
14411480
test(`ai panel continues being open when switching to code submode`, async function (assert) {
14421481
await visitOperatorMode({
14431482
stacks: [

packages/realm-server/handlers/handle-fetch-user.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import type { RealmServerTokenClaim } from '../utils/jwt';
1010
import {
1111
getCurrentActiveSubscription,
12-
getLastDailyCreditGrantAt,
12+
getDailyCreditGrantInfo,
1313
getMostRecentSubscriptionCycle,
1414
getPlanById,
1515
getUserByMatrixUserId,
@@ -35,6 +35,7 @@ type FetchUserResponse = {
3535
lowCreditThreshold: number | null;
3636
lastDailyCreditGrantAt: number | null;
3737
nextDailyCreditGrantAt: number | null;
38+
dailyCreditGrantCount: number;
3839
};
3940
relationships: {
4041
subscription: {
@@ -110,10 +111,12 @@ export default function handleFetchUserRequest({
110111
let creditsAvailableInPlanAllowance: number | null = null;
111112
let creditsIncludedInPlanAllowance: number | null = null;
112113
let extraCreditsAvailableInBalance: number | null = null;
113-
let [lastDailyCreditGrantAt, lowCreditThreshold] = await Promise.all([
114-
getLastDailyCreditGrantAt(dbAdapter, user.id),
114+
let [dailyCreditGrantInfo, lowCreditThreshold] = await Promise.all([
115+
getDailyCreditGrantInfo(dbAdapter, user.id),
115116
Promise.resolve(getLowCreditThreshold()),
116117
]);
118+
let { lastDailyCreditGrantAt, dailyCreditGrantCount } =
119+
dailyCreditGrantInfo;
117120

118121
if (currentSubscriptionCycle) {
119122
[
@@ -168,6 +171,7 @@ export default function handleFetchUserRequest({
168171
lowCreditThreshold,
169172
lastDailyCreditGrantAt,
170173
nextDailyCreditGrantAt,
174+
dailyCreditGrantCount,
171175
},
172176
relationships: {
173177
subscription: currentActiveSubscription

packages/realm-server/tests/realm-endpoints/user-test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ module(`realm-endpoints/${basename(__filename)}`, function () {
119119
lastDailyCreditGrantAt: null,
120120
nextDailyCreditGrantAt:
121121
json.data.attributes.nextDailyCreditGrantAt,
122+
dailyCreditGrantCount: 0,
122123
},
123124
relationships: {
124125
subscription: null,
@@ -245,6 +246,7 @@ module(`realm-endpoints/${basename(__filename)}`, function () {
245246
lastDailyCreditGrantAt: null,
246247
nextDailyCreditGrantAt:
247248
json.data.attributes.nextDailyCreditGrantAt,
249+
dailyCreditGrantCount: 0,
248250
},
249251
relationships: {
250252
subscription: {
@@ -353,6 +355,11 @@ module(`realm-endpoints/${basename(__filename)}`, function () {
353355
json.data.attributes.lastDailyCreditGrantAt,
354356
'lastDailyCreditGrantAt is set',
355357
);
358+
assert.strictEqual(
359+
json.data.attributes.dailyCreditGrantCount,
360+
1,
361+
'dailyCreditGrantCount reflects one daily grant entry',
362+
);
356363
});
357364

358365
test('responds without daily grant timestamps when low credit threshold is unset', async function (assert) {
@@ -431,6 +438,11 @@ module(`realm-endpoints/${basename(__filename)}`, function () {
431438
2000,
432439
'lastDailyCreditGrantAt reflects the most recent daily grant',
433440
);
441+
assert.strictEqual(
442+
json.data.attributes.dailyCreditGrantCount,
443+
2,
444+
'dailyCreditGrantCount reflects two daily grant entries',
445+
);
434446
});
435447
});
436448

0 commit comments

Comments
 (0)