Skip to content

fix(mobile): send Add credits from an organization chat to the organization's top-up - #10611

Merged
iscekic merged 5 commits into
mainfrom
drain/chat-org-add-credits
Nov 12, 2026
Merged

iscekic merged 5 commits into
mainfrom
drain/chat-org-add-credits

Conversation

@iscekic

@iscekic iscekic commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

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:

  • an organization's chat opens the organization billing page the organization hub offers for the same balance;
  • a person's chat keeps the in-app purchase screen;
  • a place that has not resolved yet offers no action instead of guessing the person's own balance.

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

  • Mounted tests: the chat screen's Add credits opens the organization billing page for an organization place, opens the in-app purchase screen for a personal place, draws no action while the place is unresolved, and draws no organization top-up where the platform may not open it. Rendering through the shared element also keeps the message footer's and the composer row's tests green.
  • The full mobile suite passes (unit and mounted).
  • Device run, local dev stack, owned fake account, one organization at an empty balance, and a personal balance. One dev-client build served both revisions from their own prepared worktrees, and each revision's own bundle was loaded before its capture:
Step Merge base This head
Organization chat, send refused failure row with Add credits failure row with Add credits
Press Add credits in-app purchase screen with the personal balance browser opens the organization billing page
Organization billing page — the organization's own page: organization details, its current balance and its credit packs
Personal chat, press Add credits not run on base in-app purchase screen

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.

  1. Question: iOS org chat: In an organization-paid chat on iOS, App Store review forbids an in-app link to the web billing page (the app already hides the organization top-up on iOS in the hub and the Profile card). What should Add credits do there?
    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)").
  2. Question: Payer loading: While the chat's payer context is not resolved yet (useChatPlace() returns null during account restore), what should the Add credits action do?
    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)
  3. Question: Non-billing member: On Android, a member whose organization role cannot manage billing (role member) gets a refused org-paid send. What should Add credits do?
    Answer: Open the organization payment-details page anyway
    Confidence: 0.65 · Precedent: Speed: choose the simplest design that reaches the goal
  4. Question: Design gate: This change keeps every rendered element and all copy identical; only the press destination changes. Run the design gate?
    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.

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.
@iscekic iscekic added drain Work item from the findings drain explorer:android Finding surfaced by the Android explorer labels Nov 12, 2026
@iscekic

iscekic commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

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.

  • Merge base: in an organization chat, the refused send's Add credits opened the in-app purchase screen with the personal balance, while the organization that refused the send stayed empty.
  • This head: the same refusal's Add credits opens the organization's own billing page (organization details, its current balance, and its credit packs).
  • This head: in a personal chat, the same refusal still opens the in-app purchase screen.
  • This head: the refused send row, measured before the tap.

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.

Merge base: Add credits in an organization chat opens the personal purchase screen

Head: the organization billing page the app opens from that refusal

Head: a personal chat still opens the in-app purchase screen

Head: the refused send row with Add credits before the tap

Design gate contact sheet for the chat credits-refusal surface

@kilo-code-bot

kilo-code-bot Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

The 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 sessionDataLoaded flag and adds a mounted test for the withheld window. The one previous finding is fixed at this HEAD.

Assumptions: [INFERENCE] none.

Files Reviewed (3 files)
  • apps/mobile/src/components/agents/session-detail-content.tsx
  • apps/mobile/src/components/agents/session-detail-content.test.ts
  • apps/mobile/src/lib/credits/add-credits-destination.ts
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 Summary

The 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: [INFERENCE] a cached transcript can render a credits-failure row while fetchedData is null, so Add credits is tappable in that window before the session's organizationId resolves.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/components/agents/session-detail-content.tsx 1391 An unresolved session (fetchedData null while a cached transcript renders) maps to the in-app personal purchase screen instead of withholding the action
Files Reviewed (3 files)
  • apps/mobile/src/components/agents/session-detail-content.tsx - 1 issue
  • apps/mobile/src/lib/credits/add-credits-destination.ts - no issues
  • apps/mobile/src/lib/credits/add-credits-destination.test.ts - no issues

Fix these issues in Kilo Cloud

Previous review (commit b8f4374)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The 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: [INFERENCE] organization-scoped agent sessions draw their sends from the organization balance, because mobile-session-manager.ts sends through organizations.cloudAgentNext.sendMessage when the session carries an organizationId.

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
apps/mobile/src/components/agents/session-detail-content.tsx 1391 Add credits in an organization-scoped agent session still opens the personal purchase screen instead of the organization billing page
Files Reviewed (15 files)
  • apps/mobile/src/components/add-credits-row.tsx - no issues
  • apps/mobile/src/components/agents/message-bubble-accessibility.test.ts - no issues
  • apps/mobile/src/components/agents/message-bubble.test.ts - no issues
  • apps/mobile/src/components/agents/message-bubble.tsx - no issues
  • apps/mobile/src/components/agents/session-detail-content.tsx - 1 issue
  • apps/mobile/src/components/agents/transcript-style-fixture.tsx - no issues
  • apps/mobile/src/components/chat/chat-composer-area.tsx - no issues
  • apps/mobile/src/components/chat/chat-failure-row.tsx - no issues
  • apps/mobile/src/components/chat/chat-screen.mounted.test.tsx - no issues
  • apps/mobile/src/components/chat/chat-screen.tsx - no issues
  • apps/mobile/src/components/chat/use-chat-add-credits.ts - no issues
  • apps/mobile/src/components/chat/use-chat-message-actions.ts - no issues
  • apps/mobile/src/components/organization/hub-screen.tsx - no issues
  • apps/mobile/src/components/profile-credits-card.tsx - no issues
  • apps/mobile/src/lib/credits/add-credits-destination.ts - no issues

Reviewed by deepseek-flash-latest · Input: 85.8K · Output: 10.8K · Cached: 1M

Review guidance: REVIEW.md from base branch main

@eshurakov eshurakov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@iscekic

iscekic commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Re-ran the device proof and the design gate at the current head after the session-surface commit.

  • Android emulator, local dev stack, same owned fake account: the organization chat's refused send shows the failure row with Add credits, and pressing it opens the organization's own billing page with its balance and credit packs.
  • Design gate at this head: the chat credits-refusal surface, 4 cells, 0 errors, 0 warnings; the prepared base run reports the same, and the comparison found no introduced findings.

Current head: the refused organization send with Add credits

Current head: the organization billing page Add credits opens

Current head design gate contact sheet for the chat credits-refusal surface

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.
@iscekic

iscekic commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Proof refreshed at the current head from the last review fix.

  • Android emulator, local dev stack, same owned fake account: the organization chat's refused send shows the failure row with Add credits, and pressing it opens the organization's own billing page; the web app logged that page's request answering 200.
  • Design gate at this head: the chat credits-refusal surface, 4 cells, 0 errors, 0 warnings; the prepared base run reports the same, and the comparison found no introduced findings.

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.

Current head: the refused organization send with Add credits

Current head: the organization billing page Add credits opens

Current head design gate contact sheet for the chat credits-refusal surface

@iscekic iscekic added merge:human Substantive functional risk requires human review. merge:blocked:design-review Design review has not cleared this change and removed merge:blocked:design-review Design review has not cleared this change labels Nov 12, 2026
@iscekic iscekic removed the merge:human Substantive functional risk requires human review. label Nov 12, 2026
@iscekic

iscekic commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

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.

  • Merge base, organization chat whose send the organization's balance refused: the failure row offers Add credits and Copy to composer, and Add credits opens the in-app purchase screen reading the personal balance, while the organization that paid and refused the send has nothing.
  • This head, same organization chat and same refusal: Add credits opens the organization's billing page, which shows that organization's own balance and its credit packs.
  • This head, a personal chat whose send the personal balance refused: Add credits still opens the in-app purchase screen.

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.

Merge base: Add credits in an organization chat opens the personal purchase screen

This head: the organization chat refuses the send and offers Add credits

This head: Add credits opens that organization billing page with its own balance

This head: a personal chat still opens the in-app purchase screen

Design gate contact sheet for the chat credits-refusal surface

@iscekic iscekic added merge:human Substantive functional risk requires human review. merge:blocked:design-review Design review has not cleared this change and removed merge:blocked:design-review Design review has not cleared this change labels Nov 12, 2026

@eshurakov eshurakov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@iscekic

iscekic commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

bot: Re: the review body remark on addCreditsDestinationOf and organizationIdOf.

addCreditsDestinationOf keeps its own org.kind === 'organization' ? org.id : undefined, because the import chain is not shared at runtime. lib/chat/scope.ts imports ChatOrg from lib/chat/layers.ts as a type only, and the build erases that import. So lib/credits/add-credits-destination.ts carries no runtime dependency on the chat runtime assembly today.

lib/chat/layers.ts imports effect, @kilocode/harness-sdk, the Expo store plugin, expo-crypto, expo-sqlite, the auth token owner, and the chat fetch and backend modules. A value import of organizationIdOf would put all of that behind the shared Add credits element and its pure unit test. lib/chat/layers.test.ts shows that cost: it stubs a dozen device modules before it can import that file.

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 lib/chat/layers.ts imports from too. That refactor changes the chat runtime assembly, so it belongs in its own change rather than in this fix.

@iscekic iscekic added merge:blocked:design-review Design review has not cleared this change and removed merge:blocked:design-review Design review has not cleared this change labels Nov 12, 2026
…edits

# Conflicts:
#	apps/mobile/src/components/profile-credits-card.tsx
@iscekic iscekic removed the merge:human Substantive functional risk requires human review. label Nov 12, 2026
@iscekic
iscekic added this pull request to the merge queue Nov 12, 2026
Merged via the queue into main with commit e83a112 Nov 12, 2026
57 of 58 checks passed
@iscekic
iscekic deleted the drain/chat-org-add-credits branch November 12, 2026 16:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

drain Work item from the findings drain explorer:android Finding surfaced by the Android explorer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants