mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-11 16:22:40 +02:00
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:
parent
290dc54d1e
commit
7fe3f211a8
4 changed files with 51 additions and 7 deletions
|
|
@ -405,6 +405,7 @@ export function SidebarContentPanel({
|
|||
const connectorsButtonRef = useRef<HTMLButtonElement | null>(null)
|
||||
const [isRowboatConnected, setIsRowboatConnected] = useState(false)
|
||||
const [loggingIn, setLoggingIn] = useState(false)
|
||||
const [appUrl, setAppUrl] = useState<string | null>(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 ? (
|
||||
<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">
|
||||
<span className="text-xs font-medium capitalize text-sidebar-foreground">
|
||||
{billing.subscriptionPlan ?? 'Free'} plan
|
||||
</span>
|
||||
<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">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-xs font-medium capitalize text-sidebar-foreground">
|
||||
{billing.subscriptionPlan ?? 'Free'} plan
|
||||
</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
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<BillingInfo | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | 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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<BillingInfo> {
|
|||
billing: {
|
||||
plan: string | null;
|
||||
status: string | null;
|
||||
trialDaysRemaining: number | null;
|
||||
usage: {
|
||||
sanctionedCredits: number;
|
||||
availableCredits: number;
|
||||
|
|
@ -37,6 +39,7 @@ export async function getBillingInfo(): Promise<BillingInfo> {
|
|||
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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue