mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-12 00:32:38 +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 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>
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue