From 7fe3f211a8f4dc3c4b61da9aeb381a4a986d3db2 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Tue, 7 Apr 2026 09:36:05 +0530 Subject: [PATCH] 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) --- .../src/components/sidebar-content.tsx | 30 +++++++++++++++---- apps/x/apps/renderer/src/hooks/useBilling.ts | 24 +++++++++++++-- apps/x/packages/core/src/billing/billing.ts | 3 ++ apps/x/packages/shared/src/ipc.ts | 1 + 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/apps/x/apps/renderer/src/components/sidebar-content.tsx b/apps/x/apps/renderer/src/components/sidebar-content.tsx index 4442c01b..55fad86d 100644 --- a/apps/x/apps/renderer/src/components/sidebar-content.tsx +++ b/apps/x/apps/renderer/src/components/sidebar-content.tsx @@ -405,6 +405,7 @@ export function SidebarContentPanel({ const connectorsButtonRef = useRef(null) const [isRowboatConnected, setIsRowboatConnected] = useState(false) const [loggingIn, setLoggingIn] = useState(false) + const [appUrl, setAppUrl] = useState(null) const { billing } = useBilling(isRowboatConnected) const handleRowboatLogin = useCallback(async () => { @@ -427,13 +428,20 @@ export function SidebarContentPanel({ const result = await window.ipc.invoke('oauth:getState', null) const config = result.config || {} const hasError = Object.values(config).some((entry) => Boolean(entry?.error)) + const connected = config['rowboat']?.connected ?? false if (mounted) { setHasOauthError(hasError) - setIsRowboatConnected(config['rowboat']?.connected ?? false) + setIsRowboatConnected(connected) if (!hasError) { 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) { console.error('Failed to fetch OAuth state:', error) if (mounted) { @@ -507,10 +515,22 @@ export function SidebarContentPanel({ {isRowboatConnected && billing ? (
- - {billing.subscriptionPlan ?? 'Free'} plan - -
diff --git a/apps/x/apps/renderer/src/hooks/useBilling.ts b/apps/x/apps/renderer/src/hooks/useBilling.ts index bf56980b..b43a5ebe 100644 --- a/apps/x/apps/renderer/src/hooks/useBilling.ts +++ b/apps/x/apps/renderer/src/hooks/useBilling.ts @@ -1,17 +1,21 @@ -import { useState, useEffect, useCallback } from 'react' +import { useState, useEffect, useCallback, useRef } from 'react' interface BillingInfo { userEmail: string | null userId: string | null subscriptionPlan: string | null subscriptionStatus: string | null + trialDaysRemaining: number | null sanctionedCredits: number availableCredits: number } +const POLLING_INTERVAL_MS = 5 * 60 * 1000 // 5 minutes + export function useBilling(isRowboatConnected: boolean) { const [billing, setBilling] = useState(null) const [isLoading, setIsLoading] = useState(false) + const intervalRef = useRef | null>(null) const fetchBilling = useCallback(async () => { if (!isRowboatConnected) { @@ -32,7 +36,23 @@ export function useBilling(isRowboatConnected: boolean) { useEffect(() => { 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 } } diff --git a/apps/x/packages/core/src/billing/billing.ts b/apps/x/packages/core/src/billing/billing.ts index b3011130..7033cd16 100644 --- a/apps/x/packages/core/src/billing/billing.ts +++ b/apps/x/packages/core/src/billing/billing.ts @@ -6,6 +6,7 @@ export interface BillingInfo { userId: string | null; subscriptionPlan: string | null; subscriptionStatus: string | null; + trialDaysRemaining: number | null; sanctionedCredits: number; availableCredits: number; } @@ -26,6 +27,7 @@ export async function getBillingInfo(): Promise { billing: { plan: string | null; status: string | null; + trialDaysRemaining: number | null; usage: { sanctionedCredits: number; availableCredits: number; @@ -37,6 +39,7 @@ export async function getBillingInfo(): Promise { userId: body.user.id ?? null, subscriptionPlan: body.billing.plan, subscriptionStatus: body.billing.status, + trialDaysRemaining: body.billing.trialDaysRemaining ?? null, sanctionedCredits: body.billing.usage.sanctionedCredits, availableCredits: body.billing.usage.availableCredits, }; diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 7e32a4fc..f2a012cf 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -564,6 +564,7 @@ const ipcSchemas = { userId: z.string().nullable(), subscriptionPlan: z.string().nullable(), subscriptionStatus: z.string().nullable(), + trialDaysRemaining: z.number().nullable(), sanctionedCredits: z.number(), availableCredits: z.number(), }),