fix(billing): redesign subscription cards and polish billing page UI#492
Conversation
Replace static "Settings" buttons on Screen Intelligence, Text Auto-Complete, and Voice Intelligence skill cards with live status dots, labels, and dynamic CTA buttons (Enable/Setup/Manage/Retry) matching third-party skill UX. Each built-in skill gets: - A status hook deriving card state from core RPC snapshots - A setup/enable modal with step-by-step flows (permissions, enable, success) - Escape key + aria dialog attributes for accessibility Screen Intelligence: permission grant flow → enable → success Text Auto-Complete: one-click enable → success Voice Intelligence: STT model check → enable voice server → success
…e UI - Redesign plan cards with clear visual hierarchy: name/tagline left, prominent price right, vertical feature checklist with check/X icons, full-width CTA buttons - Add "Popular" badge to Basic plan with accent border and shadow - Add taglines and rewrite features to user-friendly language - Remove confusing technical pills (monthly budget, 7-day cycle, 10-hour cap, discount %) - Hide "Premium-usage discount: 0%" pill for free users - Remove redundant "Why upgrade?" section - Fix double padding (SubscriptionPlans px-4/mx-4 and AutoRecharge px-4 inside parent p-4) - Fix "5-hour cap" label to "10-hour cap" and hide when both values are zero - Fix progress bar background from dark stone-700/60 to light stone-200 - Shorten verbose copy across Current Plan header, divider, and Pay as You Go description
📝 WalkthroughWalkthroughThis PR updates billing UI with simplified messaging and layout restructuring, introduces three new skill setup modal components for voice, screen intelligence, and autocomplete, and adds corresponding status hooks that derive normalized UI state from core runtime snapshots. The Skills page integrates these new modals and status hooks to manage setup flows for built-in skills. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Modal as ScreenIntelligence<br/>SetupModal
participant Core as Core API
participant State as Core State
User->>Modal: Open modal
rect rgba(100, 150, 200, 0.5)
Note over Modal: Permissions Step
User->>Modal: Request permission
Modal->>Core: requestPermission(screen_recording)
Core-->>Modal: Permission updated
Modal->>Modal: Check if all permissions granted
end
rect rgba(150, 150, 100, 0.5)
Note over Modal: Enable Step (auto-advance on all permissions)
User->>Modal: Click "Enable Screen Intelligence"
Modal->>Core: openhumanUpdateScreenIntelligenceSettings({enabled: true})
Core-->>Modal: Success
Modal->>Core: refreshStatus()
Core-->>State: Updated runtime state
Modal->>Modal: Transition to success
end
rect rgba(150, 200, 100, 0.5)
Note over Modal: Success Step
Modal-->>User: Show confirmation
User->>Modal: Close or navigate to settings
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
app/src/components/settings/panels/billing/InferenceBudget.tsx (1)
42-45: Consider aliasing legacycycleLimit5hrto a 10-hour semantic local name.Line 44 shows “10-hour cap” while reading
cycleLimit5hr; a local alias would reduce maintenance confusion.♻️ Optional clarity refactor
- {((teamUsage.cycleLimit5hr ?? 0) > 0 || (teamUsage.fiveHourCapUsd ?? 0) > 0) && ( + {(() => { + const cycleLimit10hrUsd = teamUsage.cycleLimit5hr ?? 0; + const tenHourCapUsd = teamUsage.fiveHourCapUsd ?? 0; + return (cycleLimit10hrUsd > 0 || tenHourCapUsd > 0) && ( <span className="text-[11px] text-stone-500"> - 10-hour cap: ${(teamUsage.cycleLimit5hr ?? 0).toFixed(2)} / $ - {(teamUsage.fiveHourCapUsd ?? 0).toFixed(2)} + 10-hour cap: ${cycleLimit10hrUsd.toFixed(2)} / ${tenHourCapUsd.toFixed(2)} </span> - )} + ); + })()}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/components/settings/panels/billing/InferenceBudget.tsx` around lines 42 - 45, The label "10-hour cap" uses the legacy field teamUsage.cycleLimit5hr which creates semantic confusion; introduce a local alias (e.g., tenHourCap or tenHourCycleLimit) at the top of the InferenceBudget component that maps to teamUsage.cycleLimit5hr and use that alias in the JSX and any related calculations so the name matches the displayed "10-hour" semantics and improves maintainability.app/src/components/skills/ScreenIntelligenceSetupModal.tsx (1)
116-120: Consider deriving step from permissions state instead of useEffect transition.This
useEffectcallssetStepwhenallGrantedchanges, which could be flagged byreact-hooks/set-state-in-effect. While this auto-advance pattern is common, you could alternatively derive the effective step:const effectiveStep = step === 'permissions' && allGranted ? 'enable' : step;However, if this pattern passes your lint rules and works correctly for the modal flow, it's acceptable since it represents a deliberate state machine transition rather than the problematic "reset state at effect start" anti-pattern.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/components/skills/ScreenIntelligenceSetupModal.tsx` around lines 116 - 120, The useEffect that advances step from 'permissions' to 'enable' by calling setStep when allGranted changes can be replaced by deriving the modal's displayed step to avoid state updates inside effects; instead compute an effectiveStep (e.g., using step, allGranted and the 'permissions' string) and use that for rendering and control flow, removing the useEffect and references to setStep in this transition path (keep setStep only for explicit user-driven changes); update any places reading step to use effectiveStep so the modal auto-advances without calling setStep inside the useEffect.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/src/components/settings/panels/billing/AutoRechargeSection.tsx`:
- Around line 161-165: The weekly usage text in AutoRechargeSection uses
arSettings.spentThisWeekUsd.toFixed(2) but prints arSettings.weeklyLimitUsd raw,
causing inconsistent currency precision; update the JSX that renders the span in
AutoRechargeSection (where arSettings.spentThisWeekUsd and
arSettings.weeklyLimitUsd are referenced) to format both values consistently
(e.g., call .toFixed(2) or use a shared formatCurrency utility) so the spent and
weekly limit show the same decimal precision and consistent currency
presentation.
- Around line 86-100: The toggle and other action buttons in
AutoRechargeSection.tsx are missing explicit types (defaulting to submit) which
can accidentally submit surrounding forms; update every <button> element in this
file to include type="button" — start with the toggle button that uses
onArToggle and references arSaving/arSettings and then add the same attribute to
all other action buttons in this component so none will act as form submitters.
In `@app/src/components/settings/panels/billing/SubscriptionPlans.tsx`:
- Around line 109-135: The two decorative SVGs rendered inside the
SubscriptionPlans component (the check icon and the X icon in the f.included
conditional) should be hidden from assistive tech; add aria-hidden="true" to
both <svg> elements (the ones rendering the check path "M5 13l4 4L19 7" and the
X path "M6 18L18 6M6 6l12 12") so screen readers ignore these decorative icons
while preserving their visual appearance.
---
Nitpick comments:
In `@app/src/components/settings/panels/billing/InferenceBudget.tsx`:
- Around line 42-45: The label "10-hour cap" uses the legacy field
teamUsage.cycleLimit5hr which creates semantic confusion; introduce a local
alias (e.g., tenHourCap or tenHourCycleLimit) at the top of the InferenceBudget
component that maps to teamUsage.cycleLimit5hr and use that alias in the JSX and
any related calculations so the name matches the displayed "10-hour" semantics
and improves maintainability.
In `@app/src/components/skills/ScreenIntelligenceSetupModal.tsx`:
- Around line 116-120: The useEffect that advances step from 'permissions' to
'enable' by calling setStep when allGranted changes can be replaced by deriving
the modal's displayed step to avoid state updates inside effects; instead
compute an effectiveStep (e.g., using step, allGranted and the 'permissions'
string) and use that for rendering and control flow, removing the useEffect and
references to setStep in this transition path (keep setStep only for explicit
user-driven changes); update any places reading step to use effectiveStep so the
modal auto-advances without calling setStep inside the useEffect.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: cfa14a9e-d325-4f6f-8536-811390b18b5a
📒 Files selected for processing (13)
app/src/components/settings/panels/BillingPanel.tsxapp/src/components/settings/panels/billing/AutoRechargeSection.tsxapp/src/components/settings/panels/billing/InferenceBudget.tsxapp/src/components/settings/panels/billing/PayAsYouGoCard.tsxapp/src/components/settings/panels/billing/SubscriptionPlans.tsxapp/src/components/settings/panels/billingHelpers.tsapp/src/components/skills/AutocompleteSetupModal.tsxapp/src/components/skills/ScreenIntelligenceSetupModal.tsxapp/src/components/skills/VoiceSetupModal.tsxapp/src/features/autocomplete/useAutocompleteSkillStatus.tsapp/src/features/screen-intelligence/useScreenIntelligenceSkillStatus.tsapp/src/features/voice/useVoiceSkillStatus.tsapp/src/pages/Skills.tsx
| <button | ||
| onClick={onArToggle} | ||
| disabled={arSaving} | ||
| role="switch" | ||
| aria-checked={arSettings?.enabled ?? false} | ||
| aria-label="Toggle auto-recharge" | ||
| className={`relative w-10 h-5 rounded-full transition-colors focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 focus-visible:ring-offset-stone-900 ${ | ||
| arSaving ? 'opacity-50 cursor-not-allowed' : '' | ||
| } ${arSettings?.enabled ? 'bg-primary-500' : 'bg-stone-600'}`}> | ||
| <span | ||
| className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform ${ | ||
| arSettings?.enabled ? 'translate-x-5' : 'translate-x-0' | ||
| }`} | ||
| /> | ||
| </button> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python - <<'PY'
import re
from pathlib import Path
for path in Path(".").rglob("*.tsx"):
text = path.read_text(encoding="utf-8", errors="ignore")
for m in re.finditer(r"<button\b([^>]*)>", text, flags=re.S):
attrs = m.group(1)
if re.search(r"\btype\s*=", attrs) is None:
line = text.count("\n", 0, m.start()) + 1
print(f"{path}:{line}: <button> missing explicit type")
PYRepository: tinyhumansai/openhuman
Length of output: 14627
Add explicit type="button" on all action buttons.
Several buttons currently lack explicit type, which defaults to type="submit". If this section is nested in a form, clicks can unintentionally trigger form submission.
Buttons missing type in this file: lines 86, 120, 204, 223, 242, 265, 283, 360, 370, 376, 383.
Example fix
-<button
+<button type="button"
onClick={onArToggle}
disabled={arSaving}
role="switch"
aria-checked={arSettings?.enabled ?? false}
aria-label="Toggle auto-recharge"Apply the same pattern to all other buttons in the file.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <button | |
| onClick={onArToggle} | |
| disabled={arSaving} | |
| role="switch" | |
| aria-checked={arSettings?.enabled ?? false} | |
| aria-label="Toggle auto-recharge" | |
| className={`relative w-10 h-5 rounded-full transition-colors focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 focus-visible:ring-offset-stone-900 ${ | |
| arSaving ? 'opacity-50 cursor-not-allowed' : '' | |
| } ${arSettings?.enabled ? 'bg-primary-500' : 'bg-stone-600'}`}> | |
| <span | |
| className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform ${ | |
| arSettings?.enabled ? 'translate-x-5' : 'translate-x-0' | |
| }`} | |
| /> | |
| </button> | |
| <button | |
| type="button" | |
| onClick={onArToggle} | |
| disabled={arSaving} | |
| role="switch" | |
| aria-checked={arSettings?.enabled ?? false} | |
| aria-label="Toggle auto-recharge" | |
| className={`relative w-10 h-5 rounded-full transition-colors focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2 focus-visible:ring-offset-stone-900 ${ | |
| arSaving ? 'opacity-50 cursor-not-allowed' : '' | |
| } ${arSettings?.enabled ? 'bg-primary-500' : 'bg-stone-600'}`}> | |
| <span | |
| className={`absolute top-0.5 left-0.5 w-4 h-4 rounded-full bg-white shadow transition-transform ${ | |
| arSettings?.enabled ? 'translate-x-5' : 'translate-x-0' | |
| }`} | |
| /> | |
| </button> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/src/components/settings/panels/billing/AutoRechargeSection.tsx` around
lines 86 - 100, The toggle and other action buttons in AutoRechargeSection.tsx
are missing explicit types (defaulting to submit) which can accidentally submit
surrounding forms; update every <button> element in this file to include
type="button" — start with the toggle button that uses onArToggle and references
arSaving/arSettings and then add the same attribute to all other action buttons
in this component so none will act as form submitters.
| {arSettings.spentThisWeekUsd > 0 && ( | ||
| <span className="text-[10px] text-stone-400"> | ||
| ${arSettings.spentThisWeekUsd.toFixed(2)} of ${arSettings.weeklyLimitUsd} used this | ||
| week | ||
| </span> |
There was a problem hiding this comment.
Use consistent currency formatting in weekly usage copy.
Line 163 formats the spent amount to 2 decimals but not the weekly limit, which can render inconsistent money precision.
Suggested patch
- ${arSettings.spentThisWeekUsd.toFixed(2)} of ${arSettings.weeklyLimitUsd} used this
+ ${arSettings.spentThisWeekUsd.toFixed(2)} of ${arSettings.weeklyLimitUsd.toFixed(2)} used this
week📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {arSettings.spentThisWeekUsd > 0 && ( | |
| <span className="text-[10px] text-stone-400"> | |
| ${arSettings.spentThisWeekUsd.toFixed(2)} of ${arSettings.weeklyLimitUsd} used this | |
| week | |
| </span> | |
| {arSettings.spentThisWeekUsd > 0 && ( | |
| <span className="text-[10px] text-stone-400"> | |
| ${arSettings.spentThisWeekUsd.toFixed(2)} of ${arSettings.weeklyLimitUsd.toFixed(2)} used this | |
| week | |
| </span> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/src/components/settings/panels/billing/AutoRechargeSection.tsx` around
lines 161 - 165, The weekly usage text in AutoRechargeSection uses
arSettings.spentThisWeekUsd.toFixed(2) but prints arSettings.weeklyLimitUsd raw,
causing inconsistent currency precision; update the JSX that renders the span in
AutoRechargeSection (where arSettings.spentThisWeekUsd and
arSettings.weeklyLimitUsd are referenced) to format both values consistently
(e.g., call .toFixed(2) or use a shared formatCurrency utility) so the spent and
weekly limit show the same decimal precision and consistent currency
presentation.
| {f.included ? ( | ||
| <svg | ||
| className="w-4 h-4 text-sage-500 flex-shrink-0 mt-0.5" | ||
| fill="none" | ||
| stroke="currentColor" | ||
| viewBox="0 0 24 24"> | ||
| <path | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| strokeWidth={2} | ||
| d="M5 13l4 4L19 7" | ||
| /> | ||
| </svg> | ||
| ) : ( | ||
| <svg | ||
| className="w-4 h-4 text-stone-300 flex-shrink-0 mt-0.5" | ||
| fill="none" | ||
| stroke="currentColor" | ||
| viewBox="0 0 24 24"> | ||
| <path | ||
| strokeLinecap="round" | ||
| strokeLinejoin="round" | ||
| strokeWidth={2} | ||
| d="M6 18L18 6M6 6l12 12" | ||
| /> | ||
| </svg> | ||
| )} |
There was a problem hiding this comment.
Hide decorative checklist icons from assistive tech.
Line 110 and Line 123 SVGs are purely decorative; mark them aria-hidden to avoid noisy screen-reader output.
♿ Suggested fix
- <svg
+ <svg
+ aria-hidden="true"
+ focusable="false"
className="w-4 h-4 text-sage-500 flex-shrink-0 mt-0.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
@@
- <svg
+ <svg
+ aria-hidden="true"
+ focusable="false"
className="w-4 h-4 text-stone-300 flex-shrink-0 mt-0.5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {f.included ? ( | |
| <svg | |
| className="w-4 h-4 text-sage-500 flex-shrink-0 mt-0.5" | |
| fill="none" | |
| stroke="currentColor" | |
| viewBox="0 0 24 24"> | |
| <path | |
| strokeLinecap="round" | |
| strokeLinejoin="round" | |
| strokeWidth={2} | |
| d="M5 13l4 4L19 7" | |
| /> | |
| </svg> | |
| ) : ( | |
| <svg | |
| className="w-4 h-4 text-stone-300 flex-shrink-0 mt-0.5" | |
| fill="none" | |
| stroke="currentColor" | |
| viewBox="0 0 24 24"> | |
| <path | |
| strokeLinecap="round" | |
| strokeLinejoin="round" | |
| strokeWidth={2} | |
| d="M6 18L18 6M6 6l12 12" | |
| /> | |
| </svg> | |
| )} | |
| {f.included ? ( | |
| <svg | |
| aria-hidden="true" | |
| focusable="false" | |
| className="w-4 h-4 text-sage-500 flex-shrink-0 mt-0.5" | |
| fill="none" | |
| stroke="currentColor" | |
| viewBox="0 0 24 24"> | |
| <path | |
| strokeLinecap="round" | |
| strokeLinejoin="round" | |
| strokeWidth={2} | |
| d="M5 13l4 4L19 7" | |
| /> | |
| </svg> | |
| ) : ( | |
| <svg | |
| aria-hidden="true" | |
| focusable="false" | |
| className="w-4 h-4 text-stone-300 flex-shrink-0 mt-0.5" | |
| fill="none" | |
| stroke="currentColor" | |
| viewBox="0 0 24 24"> | |
| <path | |
| strokeLinecap="round" | |
| strokeLinejoin="round" | |
| strokeWidth={2} | |
| d="M6 18L18 6M6 6l12 12" | |
| /> | |
| </svg> | |
| )} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@app/src/components/settings/panels/billing/SubscriptionPlans.tsx` around
lines 109 - 135, The two decorative SVGs rendered inside the SubscriptionPlans
component (the check icon and the X icon in the f.included conditional) should
be hidden from assistive tech; add aria-hidden="true" to both <svg> elements
(the ones rendering the check path "M5 13l4 4L19 7" and the X path "M6 18L18 6M6
6l12 12") so screen readers ignore these decorative icons while preserving their
visual appearance.
Summary
p-4)stone-700/60to lightstone-200Changed files
billingHelpers.ts— Addedrecommendedandtaglinefields to PlanMeta, rewrote feature textSubscriptionPlans.tsx— New card layout, removed doubled paddingBillingPanel.tsx— Tightened copy, removed 0% discount pill and "Why upgrade?" sectionInferenceBudget.tsx— Fixed label, hid zero-value cap, fixed progress bar bgAutoRechargeSection.tsx— Removed extra padding wrapperPayAsYouGoCard.tsx— Shortened explanation textTest plan
yarn typecheckpassesyarn test:unit -- billingHelpers— all 35 tests passSummary by CodeRabbit