Files
orca/mobile/src/components/AccountUsage.tsx
c408a3d852 feat(mobile): show usage reset countdown on accounts screen (#7954)
* feat(mobile): show usage reset countdown on accounts screen

Surface the rate-limit reset time ("5h resets in 3h 54m · 7d resets in
6d 7h") under the usage bars on the mobile accounts screen, matching the
desktop status-bar tooltip copy. The resetsAt timestamps already arrive
in the accounts.subscribe snapshot; this only adds the presentation.

Claude-Session: https://claude.ai/code/session_01FvjvCsc9QoyQALqvxkvDqQ

* docs(mobile): JSDoc for new usage reset selectors

Claude-Session: https://claude.ai/code/session_01FvjvCsc9QoyQALqvxkvDqQ

* refactor(mobile): per-bar reset countdown instead of combined line

Drop the redundant "5h/7d" prefixes — each countdown now renders under
its own bar ("Resets in 3h 54m"), matching the desktop tooltip copy
exactly.

Claude-Session: https://claude.ai/code/session_01FvjvCsc9QoyQALqvxkvDqQ

* Extract shared reset-countdown formatter for desktop and mobile

- Move duration/countdown formatting out of tooltip.tsx into
  src/shared/rate-limit-reset-format.ts so mobile's account-usage-state
  can reuse it instead of a duplicated copy (with tests).
- Re-export formatResetCountdown from tooltip.tsx to avoid touching
  existing import paths.
- Resend the pairing deep link once more in start-emulator.mjs since
  the first can arrive before the Expo app's JS router is ready.

---------

Co-authored-by: kaynan <kaynan.camargo@terceiro-sky.com.br>
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-07-13 18:56:36 -07:00

129 lines
3.2 KiB
TypeScript

import { View, Text, StyleSheet, ActivityIndicator } from 'react-native'
import { colors, spacing, typography } from '../theme/mobile-theme'
// Pure types and selectors live in account-usage-state.ts (no RN imports) so
// they are unit-testable; re-exported here so existing import sites are stable.
export type {
RateLimitWindow,
ProviderRateLimits,
InactiveAccountUsage,
ClaudeAccountSummary,
CodexAccountSummary,
AccountsSnapshot,
ProviderKey,
UsageBarState
} from './account-usage-state'
export {
getActiveProviderRateLimits,
getInactiveProviderUsage,
getUsageBarState,
getWindowResetLabel,
hasActiveProviderUsage,
hasRenderableUsage
} from './account-usage-state'
// Why: matches desktop StatusBar — bars show percent used (consumption), same
// as Claude/Codex harness meters. Fresh account is empty/green; depleted is
// full/red.
export function UsageBar({
label,
usedPercent,
unavailable,
loading,
resetText
}: {
label: string
usedPercent: number | null
unavailable: boolean
loading?: boolean
resetText?: string | null
}) {
// Why: round then clamp so bar width, color, and label share one value (desktop parity).
const used = usedPercent == null ? null : Math.max(0, Math.min(100, Math.round(usedPercent)))
// Why: same consumption bands as desktop barColor (green <60, amber <80, red ≥80).
const barColor =
used == null
? colors.textMuted
: used >= 80
? colors.statusRed
: used >= 60
? colors.statusAmber
: colors.statusGreen
return (
<View style={styles.usageBarColumn}>
<View style={styles.usageBar}>
<Text style={styles.usageLabel}>{label}</Text>
<View style={styles.usageTrack}>
<View
style={[
styles.usageFill,
{
width: `${used ?? 0}%`,
backgroundColor: unavailable ? colors.textMuted : barColor
}
]}
/>
</View>
{loading ? (
<ActivityIndicator
size="small"
color={colors.textSecondary}
style={styles.usageSpinner}
/>
) : (
<Text style={styles.usageValue}>{unavailable || used == null ? '—' : `${used}%`}</Text>
)}
</View>
{resetText ? (
<Text style={styles.usageResetText} numberOfLines={1}>
{resetText}
</Text>
) : null}
</View>
)
}
const styles = StyleSheet.create({
usageBarColumn: {
flex: 1,
gap: 2
},
usageBar: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs
},
usageLabel: {
fontSize: typography.metaSize,
color: colors.textMuted,
width: 22
},
usageTrack: {
flex: 1,
height: 6,
borderRadius: 3,
backgroundColor: colors.bgRaised,
overflow: 'hidden'
},
usageFill: {
height: '100%',
borderRadius: 3
},
usageValue: {
fontSize: typography.metaSize,
color: colors.textSecondary,
width: 36,
textAlign: 'right'
},
usageSpinner: {
width: 36
},
// Why: indented past the window label so the countdown aligns with the
// start of the track above it.
usageResetText: {
fontSize: typography.metaSize,
color: colors.textMuted,
marginLeft: 22 + spacing.xs
}
})