Add trial days remaining to billing card, 5-min polling, and wire upgrade button

- Add trialDaysRemaining to IPC schema, useBilling hook, and BillingInfo interface
- Show trial countdown in sidebar billing card (e.g. "7 days left on trial")
- Add 5-minute polling interval for billing data refresh
- Wire Upgrade button to open web billing page in system browser

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
tusharmagar 2026-04-07 09:36:05 +05:30
parent 290dc54d1e
commit 7fe3f211a8
4 changed files with 51 additions and 7 deletions

View file

@ -405,6 +405,7 @@ export function SidebarContentPanel({
const connectorsButtonRef = useRef<HTMLButtonElement | null>(null) const connectorsButtonRef = useRef<HTMLButtonElement | null>(null)
const [isRowboatConnected, setIsRowboatConnected] = useState(false) const [isRowboatConnected, setIsRowboatConnected] = useState(false)
const [loggingIn, setLoggingIn] = useState(false) const [loggingIn, setLoggingIn] = useState(false)
const [appUrl, setAppUrl] = useState<string | null>(null)
const { billing } = useBilling(isRowboatConnected) const { billing } = useBilling(isRowboatConnected)
const handleRowboatLogin = useCallback(async () => { const handleRowboatLogin = useCallback(async () => {
@ -427,13 +428,20 @@ export function SidebarContentPanel({
const result = await window.ipc.invoke('oauth:getState', null) const result = await window.ipc.invoke('oauth:getState', null)
const config = result.config || {} const config = result.config || {}
const hasError = Object.values(config).some((entry) => Boolean(entry?.error)) const hasError = Object.values(config).some((entry) => Boolean(entry?.error))
const connected = config['rowboat']?.connected ?? false
if (mounted) { if (mounted) {
setHasOauthError(hasError) setHasOauthError(hasError)
setIsRowboatConnected(config['rowboat']?.connected ?? false) setIsRowboatConnected(connected)
if (!hasError) { if (!hasError) {
setShowOauthAlert(true) setShowOauthAlert(true)
} }
} }
if (connected && mounted) {
try {
const account = await window.ipc.invoke('account:getRowboat', null)
if (mounted) setAppUrl(account.config?.appUrl ?? null)
} catch { /* ignore */ }
}
} catch (error) { } catch (error) {
console.error('Failed to fetch OAuth state:', error) console.error('Failed to fetch OAuth state:', error)
if (mounted) { if (mounted) {
@ -507,10 +515,22 @@ export function SidebarContentPanel({
{isRowboatConnected && billing ? ( {isRowboatConnected && billing ? (
<div className="px-3 py-2"> <div className="px-3 py-2">
<div className="flex items-center justify-between rounded-lg border border-sidebar-border bg-sidebar-accent/20 px-3 py-2"> <div className="flex items-center justify-between rounded-lg border border-sidebar-border bg-sidebar-accent/20 px-3 py-2">
<span className="text-xs font-medium capitalize text-sidebar-foreground"> <div className="flex flex-col gap-0.5">
{billing.subscriptionPlan ?? 'Free'} plan <span className="text-xs font-medium capitalize text-sidebar-foreground">
</span> {billing.subscriptionPlan ?? 'Free'} plan
<button className="rounded-md bg-sidebar-foreground/10 px-2.5 py-1 text-[11px] font-medium text-sidebar-foreground transition-colors hover:bg-sidebar-foreground/20"> </span>
{billing.subscriptionStatus === 'trialing' && billing.trialDaysRemaining != null && (
<span className="text-[10px] text-sidebar-foreground/60">
{billing.trialDaysRemaining === 0
? 'Trial ends today'
: `${billing.trialDaysRemaining} day${billing.trialDaysRemaining === 1 ? '' : 's'} left on trial`}
</span>
)}
</div>
<button
onClick={() => appUrl && window.open(appUrl)}
className="rounded-md bg-sidebar-foreground/10 px-2.5 py-1 text-[11px] font-medium text-sidebar-foreground transition-colors hover:bg-sidebar-foreground/20"
>
Upgrade Upgrade
</button> </button>
</div> </div>

View file

@ -1,17 +1,21 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback, useRef } from 'react'
interface BillingInfo { interface BillingInfo {
userEmail: string | null userEmail: string | null
userId: string | null userId: string | null
subscriptionPlan: string | null subscriptionPlan: string | null
subscriptionStatus: string | null subscriptionStatus: string | null
trialDaysRemaining: number | null
sanctionedCredits: number sanctionedCredits: number
availableCredits: number availableCredits: number
} }
const POLLING_INTERVAL_MS = 5 * 60 * 1000 // 5 minutes
export function useBilling(isRowboatConnected: boolean) { export function useBilling(isRowboatConnected: boolean) {
const [billing, setBilling] = useState<BillingInfo | null>(null) const [billing, setBilling] = useState<BillingInfo | null>(null)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const fetchBilling = useCallback(async () => { const fetchBilling = useCallback(async () => {
if (!isRowboatConnected) { if (!isRowboatConnected) {
@ -32,7 +36,23 @@ export function useBilling(isRowboatConnected: boolean) {
useEffect(() => { useEffect(() => {
fetchBilling() fetchBilling()
}, [fetchBilling])
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
if (isRowboatConnected) {
intervalRef.current = setInterval(fetchBilling, POLLING_INTERVAL_MS)
}
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}
}, [fetchBilling, isRowboatConnected])
return { billing, isLoading, refresh: fetchBilling } return { billing, isLoading, refresh: fetchBilling }
} }

View file

@ -6,6 +6,7 @@ export interface BillingInfo {
userId: string | null; userId: string | null;
subscriptionPlan: string | null; subscriptionPlan: string | null;
subscriptionStatus: string | null; subscriptionStatus: string | null;
trialDaysRemaining: number | null;
sanctionedCredits: number; sanctionedCredits: number;
availableCredits: number; availableCredits: number;
} }
@ -26,6 +27,7 @@ export async function getBillingInfo(): Promise<BillingInfo> {
billing: { billing: {
plan: string | null; plan: string | null;
status: string | null; status: string | null;
trialDaysRemaining: number | null;
usage: { usage: {
sanctionedCredits: number; sanctionedCredits: number;
availableCredits: number; availableCredits: number;
@ -37,6 +39,7 @@ export async function getBillingInfo(): Promise<BillingInfo> {
userId: body.user.id ?? null, userId: body.user.id ?? null,
subscriptionPlan: body.billing.plan, subscriptionPlan: body.billing.plan,
subscriptionStatus: body.billing.status, subscriptionStatus: body.billing.status,
trialDaysRemaining: body.billing.trialDaysRemaining ?? null,
sanctionedCredits: body.billing.usage.sanctionedCredits, sanctionedCredits: body.billing.usage.sanctionedCredits,
availableCredits: body.billing.usage.availableCredits, availableCredits: body.billing.usage.availableCredits,
}; };

View file

@ -564,6 +564,7 @@ const ipcSchemas = {
userId: z.string().nullable(), userId: z.string().nullable(),
subscriptionPlan: z.string().nullable(), subscriptionPlan: z.string().nullable(),
subscriptionStatus: z.string().nullable(), subscriptionStatus: z.string().nullable(),
trialDaysRemaining: z.number().nullable(),
sanctionedCredits: z.number(), sanctionedCredits: z.number(),
availableCredits: z.number(), availableCredits: z.number(),
}), }),