Skip to content

feat(mobile): organization management screens#4491

Merged
iscekic merged 13 commits into
mainfrom
mobile-org-management
Jul 10, 2026
Merged

feat(mobile): organization management screens#4491
iscekic merged 13 commits into
mainfrom
mobile-org-management

Conversation

@iscekic

@iscekic iscekic commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Adds role-gated organization management to the mobile app, reached from the profile screen when an org is selected: Manage organization (owner / billing manager) or View organization (member).

What's included

Hub screen (/organization)

  • Org info card: name (owner can rename, optimistic), balance + seats (owner/billing only), 30-day usage stats (all roles)
  • Drill-in rows: Members (all roles); Credit activity, Invoices, Low balance alert (owner/billing only)

Members screen

  • Active members + pending invitations, sorted, role badges, daily-limit hints
  • Invite (owner: any role; billing manager: member only, no picker) via form sheet
  • Owner-only member actions (never on own row): change role, set/clear daily usage limit (hidden when enable_usage_limits === false), remove member (destructive confirm)
  • Pending invite actions (owner): share invite link via native share sheet, revoke

Read-only money screens (owner/billing): credit activity list, invoices list — deliberately no tap actions and zero Stripe surface (App Store review)

Form sheets: invite member / daily usage limit / low balance alert — native formSheet presentation, inline mutation error display (toasts render behind native sheets — see notes)

All operations call existing tRPC procedures (organizations.*); server enforces permissions, client gating is visibility only. Caller role derives from the already-cached organizations.list. No backend changes.

Notable fixes found during e2e

  • organizations.list balance is microdollars — was rendered as dollars
  • sonner-native toasts render behind iOS formSheets, so mutation errors inside sheets were silent → sheets now render the error inline (the same latent issue exists in pre-existing security-agent sheets)
  • Org-scoped query hooks gate on a resolved org id (no more empty-string queries while org context loads)

Verification (local stack, iOS simulator, all three roles)

  • Owner: rename (optimistic + DB), low-balance alert config (DB settings verified), invite with 3-role picker → pending invite → share sheet → revoke, role change Member↔Billing manager (UI + DB), daily limit set/remove (server round-trip), remove member (UI + DB), credit activity row, invoices empty state, self-row inert, invite-conflict error shown inline (server 409)
  • Billing manager: balance visible, no rename pencil, invite locked to Member with no picker, member rows inert
  • Member: "View organization", hub shows name/seats/usage/Members only (zero money UI), members list read-only, no invite button
  • pnpm format && pnpm typecheck && pnpm lint && pnpm check:unused all clean

iscekic added 12 commits July 10, 2026 13:45
Data layer for upcoming org-management screens (hub, members, credit
activity, invoices): thin tRPC query hooks plus optimistic mutations
for renaming an org, inviting/updating/removing members, deleting
invites, and toggling the minimum balance alert.
Adds the org hub route and its screen: info card (name + rename,
balance for money roles, seats), last-30-days usage stats, and nav
rows to members/credit-activity/invoices/low-balance-alert. Consumes
the Task 1 org data hooks; target nav routes are stubs for later tasks.
useOrgWithMembers/useOrgUsageStats/useOrgCreditTransactions/useOrgInvoices
now accept organizationId: string | null and disable the query until it
resolves, instead of firing with an empty-string id that the backend
rejects. hub-screen.tsx passes organizationId through directly. Also
default the org info-card skeleton to the 2-row shape since role (and
therefore row count) isn't known while loading.
Extract ActionTile into its own file to keep profile-screen.tsx under
the 300-line lint ceiling, then add a Manage/View org row that links
to the organization hub screen added in Task 2.
Builds the members drill-in linked from the org hub: active-member rows
with role/limit/remove management for owners, invited-member rows with
share/revoke actions, sorted lists, and an invite-member entry point in
the header for owner/billing roles.
Register invite-member, member-limit, and low-balance-alert as formSheet
routes on the organization stack, and build their sheet content.
… sheets

Member-limit sheet now shows a skeleton while loading and a "Member not
found" message instead of a blank sheet. Low-balance-alert sheet defers
mounting its form until settings load, so its enabled/threshold state
isn't seeded from stale data. Shares one EMAIL_PATTERN in lib/utils
instead of duplicating it, and blocks saving when any email in the list
is malformed instead of silently dropping it.
…screen

organizations.list returns balance in microdollars (see organizations.ts's
total_microdollars_acquired - microdollars_used), but the hub screen rendered
it directly, showing $50000000.00 for a $50 org.
Toasts are invisible under iOS formSheet screens since they're native
view controllers layered above the root layout where sonner-native
mounts. Render the mutation error inline near the submit button
instead so invite/limit/alert failures are visible while the sheet is
open. The centralized onError toast stays for non-sheet callers.
Split MemberLimitSheet's form into a child component that only mounts
once member data exists, so the limit ref/state initialize from real
data instead of the empty pre-load value. Also import ROLE_LABEL from
member-row instead of re-declaring it in invited-member-row.
@iscekic iscekic self-assigned this Jul 10, 2026
Comment thread apps/mobile/src/components/organization/hub-screen.tsx Outdated
@kilo-code-bot

kilo-code-bot Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 7 Issues Found | Recommendation: Address before merge

Executive Summary

The mobile organization feature can sign ordinary members out on direct invoice-route access and bypasses required seat-capacity, trial read-only, and plan gates in member management.

Overview

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

WARNING

File Line Issue
apps/mobile/src/components/organization/invoices-screen.tsx 65 Direct route access by an ordinary member triggers a role-based 401 that the mobile client handles by signing the user out.
apps/mobile/src/components/organization/members-screen.tsx 66 Teams organizations at seat capacity can still send seat-consuming invitations.
apps/mobile/src/components/organization/members-screen.tsx 66 Child organizations expose an invitation flow that the server always rejects.
apps/mobile/src/components/organization/members-screen.tsx 65 Teams organizations can be offered Enterprise-only daily usage-limit controls.
apps/mobile/src/components/organization/members-screen.tsx 115 Soft-expired organizations remain mutable despite the required UI-only read-only state.
apps/mobile/src/components/organization/low-balance-alert-sheet.tsx 46 A background refresh cannot update the mounted form, allowing stale settings to overwrite newer changes.
apps/mobile/src/components/organization/member-limit-sheet.tsx 48 A refreshed member limit is ignored, allowing Save to restore a stale value.
Files Reviewed (24 files)
  • apps/mobile/src/app/(app)/(tabs)/(3_profile)/organization/_layout.tsx
  • apps/mobile/src/app/(app)/(tabs)/(3_profile)/organization/credit-activity.tsx
  • apps/mobile/src/app/(app)/(tabs)/(3_profile)/organization/index.tsx
  • apps/mobile/src/app/(app)/(tabs)/(3_profile)/organization/invite-member.tsx
  • apps/mobile/src/app/(app)/(tabs)/(3_profile)/organization/invoices.tsx
  • apps/mobile/src/app/(app)/(tabs)/(3_profile)/organization/low-balance-alert.tsx
  • apps/mobile/src/app/(app)/(tabs)/(3_profile)/organization/member-limit.tsx
  • apps/mobile/src/app/(app)/(tabs)/(3_profile)/organization/members.tsx
  • apps/mobile/src/components/organization/credit-activity-screen.tsx
  • apps/mobile/src/components/organization/hub-screen.tsx
  • apps/mobile/src/components/organization/invite-member-sheet.tsx
  • apps/mobile/src/components/organization/invited-member-row.tsx
  • apps/mobile/src/components/organization/invoices-screen.tsx - 1 issue
  • apps/mobile/src/components/organization/low-balance-alert-sheet.tsx - 1 issue
  • apps/mobile/src/components/organization/member-limit-sheet.tsx - 1 issue
  • apps/mobile/src/components/organization/member-row.tsx
  • apps/mobile/src/components/organization/members-screen.tsx - 4 issues
  • apps/mobile/src/components/organization/org-usage-stats.tsx
  • apps/mobile/src/components/organization/rename-org-modal.tsx
  • apps/mobile/src/components/profile-action-tile.tsx
  • apps/mobile/src/components/profile-screen.tsx
  • apps/mobile/src/lib/hooks/use-organization-mutations.ts
  • apps/mobile/src/lib/hooks/use-organization-queries.ts
  • apps/mobile/src/lib/utils.ts

Fix these issues in Kilo Cloud

Previous Review Summary (commit dfd0be0)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit dfd0be0)

Status: 1 Issue Found | Recommendation: Address before merge

Executive Summary

The organization rename control in the mobile hub screen is gated to owner only, more restrictively than the owner-or-billing_manager permission the underlying mutation actually grants and than the rest of the screen's money-role gating.

Overview

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

WARNING

File Line Issue
apps/mobile/src/components/organization/hub-screen.tsx 63 Rename pencil is shown only to owner, but the organizations.update mutation it calls permits owner or billing_manager, and billing_manager is treated as a money role everywhere else on this screen.
Files Reviewed (24 files)
  • apps/mobile/src/app/(app)/(tabs)/(3_profile)/organization/_layout.tsx
  • apps/mobile/src/app/(app)/(tabs)/(3_profile)/organization/credit-activity.tsx
  • apps/mobile/src/app/(app)/(tabs)/(3_profile)/organization/index.tsx
  • apps/mobile/src/app/(app)/(tabs)/(3_profile)/organization/invite-member.tsx
  • apps/mobile/src/app/(app)/(tabs)/(3_profile)/organization/invoices.tsx
  • apps/mobile/src/app/(app)/(tabs)/(3_profile)/organization/low-balance-alert.tsx
  • apps/mobile/src/app/(app)/(tabs)/(3_profile)/organization/member-limit.tsx
  • apps/mobile/src/app/(app)/(tabs)/(3_profile)/organization/members.tsx
  • apps/mobile/src/components/organization/credit-activity-screen.tsx - 1 issue (not flagged; established repo-wide pattern)
  • apps/mobile/src/components/organization/hub-screen.tsx - 1 issue
  • apps/mobile/src/components/organization/invite-member-sheet.tsx
  • apps/mobile/src/components/organization/invited-member-row.tsx
  • apps/mobile/src/components/organization/invoices-screen.tsx
  • apps/mobile/src/components/organization/low-balance-alert-sheet.tsx
  • apps/mobile/src/components/organization/member-limit-sheet.tsx
  • apps/mobile/src/components/organization/member-row.tsx
  • apps/mobile/src/components/organization/members-screen.tsx
  • apps/mobile/src/components/organization/org-usage-stats.tsx
  • apps/mobile/src/components/organization/rename-org-modal.tsx
  • apps/mobile/src/components/profile-action-tile.tsx
  • apps/mobile/src/components/profile-screen.tsx
  • apps/mobile/src/lib/hooks/use-organization-mutations.ts
  • apps/mobile/src/lib/hooks/use-organization-queries.ts
  • apps/mobile/src/lib/utils.ts

Fix these issues in Kilo Cloud


Reviewed by gpt-5.6-sol · Input: 200.4K · Output: 50.3K · Cached: 7.5M

Review guidance: REVIEW.md from base branch main

@iscekic iscekic merged commit 293c0c5 into main Jul 10, 2026
19 checks passed
@iscekic iscekic deleted the mobile-org-management branch July 10, 2026 14:08

export function OrganizationInvoicesScreen() {
const { organizationId } = useOrgRole();
const query = useOrgInvoices(organizationId);

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.

WARNING: Direct route access signs ordinary members out

This query is enabled for every selected organization even though organizations.invoices requires owner or billing_manager. Expo routes remain directly addressable, and that role check throws tRPC UNAUTHORIZED (HTTP 401); the mobile query cache treats every 401 as an expired session and calls signOut. A member who opens this route directly is therefore logged out rather than denied access. Gate the query on isMoneyRole(role) before issuing it.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


const isLoading = orgWithMembers.isLoading || !orgWithMembers.data;
const enableUsageLimits = orgWithMembers.data?.settings.enable_usage_limits !== false;
const canInvite = isMoneyRole(role);

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.

WARNING: Full Teams organizations can still send invitations

canInvite only checks role, so a Teams org with requireSeats enabled still exposes the invite sheet when org.seatCount.used >= org.seatCount.total. The seat-billing contract requires the invitation UI to be disabled in that state, and the server deliberately does not enforce this UI-only limit, so mobile can create invitations beyond purchased capacity. Include the selected org's plan, requireSeats, and seat counts in this gate (while preserving billing-manager invitations, which do not consume seats).


Reply with @kilocode-bot fix it to have Kilo Code address this issue.


const isLoading = orgWithMembers.isLoading || !orgWithMembers.data;
const enableUsageLimits = orgWithMembers.data?.settings.enable_usage_limits !== false;
const canInvite = isMoneyRole(role);

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.

WARNING: Child organizations expose an invitation flow that always fails

The role-only gate also shows the invite action for child organizations. inviteUserToOrganization rejects every direct child-org invitation with Child organizations cannot invite members, and the web member UI explicitly hides this action when parent_organization_id is set. Gate the action on the loaded organization not being a child.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

}

const isLoading = orgWithMembers.isLoading || !orgWithMembers.data;
const enableUsageLimits = orgWithMembers.data?.settings.enable_usage_limits !== false;

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.

WARNING: Teams organizations can receive Enterprise-only usage-limit controls

This defaults the feature to enabled whenever enable_usage_limits is absent, without checking the organization plan. The existing organization UI only exposes daily limits when plan === 'enterprise', and the plan contract says Enterprise-only settings must not be exposed while the org is on Teams. Legacy Teams rows with a missing or retained true setting can therefore open the sheet and mutate limits because the server mutation has no plan check. Require the Enterprise plan as well as the setting.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

<MemberRow
key={member.id}
member={member}
canManage={isOwner && member.id !== currentUserId}

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.

WARNING: Soft-expired organizations remain mutable from mobile

Member actions are gated only by role and identity, and the invite action above is similarly role-only. During the soft-expired trial stage the contract requires the UI to become read-only; server middleware intentionally blocks only hard-expired mutations. The web organization surface supplies this entitlement gate through OrganizationTrialWrapper/LockableContainer, but the mobile screens do not load or apply an equivalent status, so owners can keep changing roles, limits, invitations, and membership during the read-only period.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

const mutations = useOrganizationMutations(organizationId ?? '');
const { email: myEmail } = useCurrentUserId();

const [enabled, setEnabled] = useState(settings.minimum_balance !== undefined);

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.

WARNING: A background refresh cannot update this form's snapshot

The hub normally leaves withMembers cached before this sheet opens, so the form mounts immediately from that cached object while React Query refetches it (default stale time is zero). enabled, both refs, and the uncontrolled defaultValues are initialized only once; if the refetch returns settings changed by another client, the form continues displaying and submitting the old values and can overwrite the newer configuration. Remount or explicitly reset the form when the refreshed settings change.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

const mutations = useOrganizationMutations(organizationId ?? '');
const currentLimit = member.dailyUsageLimitUsd;

const limitRef = useRef(currentLimit != null ? String(currentLimit) : '');

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.

WARNING: A refreshed member limit is ignored by the form state

This sheet opens from the same withMembers cache populated by the members screen and then refetches it. If another owner changed the limit since that cached result, member.dailyUsageLimitUsd updates after the refetch but this ref and the uncontrolled input keep the value captured at first mount; pressing Save restores the stale limit. Key/remount the form from the refreshed member value or reset its ref and input when that value changes.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants