Repository navigation
fix(mobile): send Add credits from an organization chat to the organization's top-up - #10611
Conversation
Add credits on a send the balance refused always opened the in-app purchase screen, which sells the person's own credit packs. In a chat an organization pays for, that screen showed the person's balance while the organization's balance stayed empty, so buying there could not unblock the next send. The chat's place names the balance its sends draw on. Its Add credits now carries that destination: the organization's own billing page where the organization pays, which is the page the organization hub offers, and the in-app purchase screen for the person's own balance. A place that is not resolved yet offers no action instead of guessing, and the shared Add credits element keeps its existing platform gate for the external page.
|
bot: Verified on the Android emulator against the local dev stack, with an owned fake account, one organization at an empty balance, and a personal balance.
The design gate compared the chat credits-refusal surface on the prepared head and base: 4 cells, 0 errors, 0 warnings for each, and no introduced findings. |
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Executive SummaryThe incremental commit gates the session surface's Add credits destination on the current session's own loaded metadata, so an unresolved payer no longer falls back to the personal purchase screen; the fix reuses the existing Assumptions: Files Reviewed (3 files)
Previous Review Summaries (2 snapshots, latest commit 1b4e419)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 1b4e419)Status: 1 Issue Found | Recommendation: Address before merge Executive SummaryThe session surface now routes by the session's organization, but while the session's metadata is still opening it still treats the unresolved payer as personal and can open the personal purchase screen for an organization session. Assumptions: Overview
Issue Details (click to expand)WARNING
Files Reviewed (3 files)
Fix these issues in Kilo Cloud Previous review (commit b8f4374)Status: 1 Issue Found | Recommendation: Address before merge Executive SummaryThe chat tab now settles a refused send on the balance that paid for it, but the agent session surface still routes an organization-scoped session's Add credits to the personal purchase screen. Assumptions: Overview
Issue Details (click to expand)WARNING
Files Reviewed (15 files)
Reviewed by deepseek-flash-latest · Input: 85.8K · Output: 10.8K · Cached: 1M Review guidance: REVIEW.md from base branch |
eshurakov
left a comment
There was a problem hiding this comment.
Bot - Minor: SessionDetailContent still hardcodes the in-app Add credits destination (handleAddCredits -> /(app)/credits) even though it has fetchedData.organizationId at session-detail-content.tsx:573. An organization-funded agent session whose send the organization's balance refused would still open the personal purchase screen, the same mismatch this PR fixes for chats. Consider deriving its destination with addCreditsDestinationOf from the session's organization so organization sessions open the organization billing page.
… pays for it An agent session records the organization its sends spend, and its credit refusal still opened the in-app purchase screen. Where the session names an organization, its Add credits now opens that organization's billing page; a session with none keeps the in-app purchase screen.
|
bot: Re-ran the device proof and the design gate at the current head after the session-surface commit.
|
The session screen paints its cached transcript while the current session's metadata is still on its way, and that metadata is the only record of the organization whose balance pays for the session. Reading "no organization" from a load that has not answered yet sent an organization's refusal to the personal purchase screen. The destination now exists only once the loaded record is the current session's one, the same flag the profile chip uses; until then the row offers no action. A test covers the withheld window and the destination that appears when the record names an organization.
|
bot: Proof refreshed at the current head from the last review fix.
The session surface's new load-window behaviour is covered by a mounted test: the row offers no Add credits until the current session's own record is loaded, then opens the organization's billing page when that record names one. |
|
bot: Re-ran the device proof and the design gate at the current head, after merging the base branch. One dev-client build, one local stack, one owned fake account. The merge-base bundle and the head bundle were served from their own prepared worktrees, and each revision's own bundle was loaded before its capture.
The design gate compared the chat credits-refusal surface on the prepared head and base at this head: 4 cells, 0 errors, 0 warnings for each, and the comparison is comparable with no introduced findings. |
eshurakov
left a comment
There was a problem hiding this comment.
Bot - Minor: addCreditsDestinationOf re-derives the payer from place.org.kind === 'organization' ? place.org.id : undefined; organizationIdOf(org) in apps/mobile/src/lib/chat/layers.ts already does exactly that, and this module's import chain already depends on that file. Reusing it keeps the discrimination in one place.
|
bot: Re: the review body remark on
The discrimination is one ternary over an org the placed chat has already narrowed, and the credits helper needs only the id. It stays here rather than pulling the chat runtime into the credits helper. If the discrimination should live in one place, the single home is a pure module that |
…edits # Conflicts: # apps/mobile/src/components/profile-credits-card.tsx
















Evidence
Evidence: demonstrated — on the Android emulator against the local dev stack, an organization chat's refused send opened the in-app personal purchase screen on the merge base, and opens the organization's own billing page at this head; the same chat in the personal context still opens the in-app purchase screen.
What changed
Add credits on a send the balance refused always opened the in-app purchase screen. That screen sells the person's own credit packs and reads the person's balance, so in a chat an organization pays for it showed the personal balance while the organization's balance, the one that refused the send, stayed empty. Buying there could not unblock the next send.
The chat already knows whose balance its sends draw on: its place names a personal account or an organization. Add credits now carries that balance's own destination:
The action itself is the app's shared Add credits element, so the external-page platform gate it already owns stays in one place: where a platform may not open an external purchase page, no organization top-up is offered. That element now also takes the transcript's primary emphasis, so the message footer keeps the accent surface it had, and the composer row's recovery action renders through the same control as the rest of the app.
The organization billing URL has one builder, used by the chat, the organization hub and the Profile credits card.
An agent session has the same problem: it records the organization its sends spend, and its Add credits opened the same personal purchase screen. Where the session records an organization, its Add credits now opens that organization's billing page, through the same destination helper; a session with no organization keeps the in-app purchase screen. The screen paints a cached transcript before the current session's own record arrives, so during that window the row offers no action instead of reading "no organization" from a load that has not answered.
No copy changed, so no catalog changed.
Verification
The web app logged the page the app opened for the organization chat: the request for the organization billing page, resolving to the sign-in callback path for that same page in a browser without a session.
A destination that fails to open keeps the shared external-link outcome: an error toast naming the billing page, covered by that module's tests. The loading state is covered by the mounted test above.
The session surface's destination is unit-tested (organization, personal and unresolved), and a mounted test covers its load window: no action until the current session's own record is loaded, then the organization's billing page when that record names an organization. A device run of that surface would need a cloud agent session, and the local stack this proof used has no cloud agent runtime, so that surface is covered by those tests and by the same device-proven open path it shares with the chat.
Design gate
Design gate: 4 cells, 0 errors, 0 warnings.
The prepared base run of the same target at this head also reported 4 cells, 0 errors, 0 warnings, and the comparison is comparable with no introduced findings. Both runs verified the same product bundle against the preparation receipt.
Production prerequisites
None.
Proxy answers
The owner's proxy gave these answers in the owner's place.
Answer: Offer no Add credits on iOS in org chats
Confidence: 0.70 · Precedent: Engineering conventions: reuse the existing equivalent and keep its behavior consistent ("reuse as much as possible from the current impl and routers"). Fleet: Android by default ("doesn't mean it needs to be verified on iOS (unless iOS specific)").
Answer: Withhold it until the payer is known
Confidence: 0.80 · Precedent: UX: "never treat data that has not loaded yet as empty" (PR feat(mobile): add contextual session discovery to Siri and Shortcuts #9848)
Answer: Open the organization payment-details page anyway
Confidence: 0.65 · Precedent: Speed: choose the simplest design that reaches the goal
Answer: Run the gate on the chat credits-failure surface
Confidence: 0.70 · Precedent: Product scope: keep the approved scope of a manual work item intact. The brief says "Run the design gate", and the iOS and loading choices do change what renders.