Update Electron billing UI for free plan

This commit is contained in:
Ramnique Singh 2026-05-18 11:12:10 +05:30
parent d586f6bd8a
commit af618155e1
8 changed files with 31 additions and 36 deletions

View file

@ -97,7 +97,7 @@ const BILLING_ERROR_PATTERNS = [
{ {
pattern: /not enough credits/i, pattern: /not enough credits/i,
title: 'You\'ve run out of credits', title: 'You\'ve run out of credits',
subtitle: 'Upgrade your plan for more credits, or wait for your billing cycle to reset.', subtitle: 'Upgrade your plan for more credits. Free usage resets daily at 00:00 UTC.',
cta: 'Upgrade plan', cta: 'Upgrade plan',
}, },
{ {

View file

@ -29,6 +29,7 @@ export function AccountSettings({ dialogOpen }: AccountSettingsProps) {
const [connecting, setConnecting] = useState(false) const [connecting, setConnecting] = useState(false)
const [appUrl, setAppUrl] = useState<string | null>(null) const [appUrl, setAppUrl] = useState<string | null>(null)
const { billing, isLoading: billingLoading } = useBilling(isRowboatConnected) const { billing, isLoading: billingLoading } = useBilling(isRowboatConnected)
const hasPaidSubscription = billing?.subscriptionPlan === 'starter' || billing?.subscriptionPlan === 'pro'
const checkConnection = useCallback(async () => { const checkConnection = useCallback(async () => {
try { try {
@ -178,9 +179,12 @@ export function AccountSettings({ dialogOpen }: AccountSettingsProps) {
{!billing.subscriptionPlan && ( {!billing.subscriptionPlan && (
<p className="text-xs text-muted-foreground">Subscribe to access AI features</p> <p className="text-xs text-muted-foreground">Subscribe to access AI features</p>
)} )}
{billing.subscriptionPlan === 'free' && (
<p className="text-xs text-muted-foreground">Free usage resets daily at 00:00 UTC.</p>
)}
</div> </div>
<Button variant="outline" size="sm" onClick={() => appUrl && window.open(`${appUrl}?intent=upgrade`)}> <Button variant="outline" size="sm" onClick={() => appUrl && window.open(`${appUrl}?intent=upgrade`)}>
{!billing.subscriptionPlan ? 'Subscribe' : 'Change plan'} {!billing.subscriptionPlan ? 'Subscribe' : billing.subscriptionPlan === 'free' ? 'Upgrade' : 'Change plan'}
</Button> </Button>
</div> </div>
</div> </div>
@ -203,15 +207,15 @@ export function AccountSettings({ dialogOpen }: AccountSettingsProps) {
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
disabled={!billing?.subscriptionPlan} disabled={!hasPaidSubscription}
onClick={() => appUrl && window.open(appUrl)} onClick={() => appUrl && window.open(appUrl)}
className="gap-1.5" className="gap-1.5"
> >
<ExternalLink className="size-3" /> <ExternalLink className="size-3" />
Manage in Stripe Manage in Stripe
</Button> </Button>
{!billing?.subscriptionPlan && ( {!hasPaidSubscription && (
<p className="text-[11px] text-muted-foreground">Subscribe to a plan first</p> <p className="text-[11px] text-muted-foreground">Upgrade to a paid plan first</p>
)} )}
</div> </div>

View file

@ -758,7 +758,7 @@ export function SidebarContentPanel({
onClick={() => appUrl && window.open(`${appUrl}?intent=upgrade`)} onClick={() => appUrl && window.open(`${appUrl}?intent=upgrade`)}
className="shrink-0 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" className="shrink-0 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"
> >
{!billing.subscriptionPlan ? 'Subscribe' : billing.subscriptionPlan === 'starter' ? 'Upgrade' : 'Manage'} {!billing.subscriptionPlan || billing.subscriptionPlan === 'free' || billing.subscriptionPlan === 'starter' ? 'Upgrade' : 'Manage'}
</button> </button>
</div> </div>
</div> </div>

View file

@ -1,14 +1,5 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import type { BillingInfo } from '@x/shared/dist/billing.js'
interface BillingInfo {
userEmail: string | null
userId: string | null
subscriptionPlan: string | null
subscriptionStatus: string | null
trialExpiresAt: string | null
sanctionedCredits: number
availableCredits: number
}
export function useBilling(isRowboatConnected: boolean) { export function useBilling(isRowboatConnected: boolean) {
const [billing, setBilling] = useState<BillingInfo | null>(null) const [billing, setBilling] = useState<BillingInfo | null>(null)

View file

@ -1,15 +1,6 @@
import { getAccessToken } from '../auth/tokens.js'; import { getAccessToken } from '../auth/tokens.js';
import { API_URL } from '../config/env.js'; import { API_URL } from '../config/env.js';
import type { BillingInfo, BillingPlan } from '@x/shared/dist/billing.js';
export interface BillingInfo {
userEmail: string | null;
userId: string | null;
subscriptionPlan: string | null;
subscriptionStatus: string | null;
trialExpiresAt: string | null;
sanctionedCredits: number;
availableCredits: number;
}
export async function getBillingInfo(): Promise<BillingInfo> { export async function getBillingInfo(): Promise<BillingInfo> {
const accessToken = await getAccessToken(); const accessToken = await getAccessToken();
@ -25,7 +16,7 @@ export async function getBillingInfo(): Promise<BillingInfo> {
email: string; email: string;
}; };
billing: { billing: {
plan: string | null; plan: BillingPlan | null;
status: string | null; status: string | null;
trialExpiresAt: string | null; trialExpiresAt: string | null;
usage: { usage: {

View file

@ -0,0 +1,15 @@
import { z } from 'zod';
export const BillingPlanSchema = z.enum(['free', 'starter', 'pro']);
export type BillingPlan = z.infer<typeof BillingPlanSchema>;
export const BillingInfoSchema = z.object({
userEmail: z.string().nullable(),
userId: z.string().nullable(),
subscriptionPlan: BillingPlanSchema.nullable(),
subscriptionStatus: z.string().nullable(),
trialExpiresAt: z.string().nullable(),
sanctionedCredits: z.number(),
availableCredits: z.number(),
});
export type BillingInfo = z.infer<typeof BillingInfoSchema>;

View file

@ -16,4 +16,5 @@ export * as promptBlock from './prompt-block.js';
export * as frontmatter from './frontmatter.js'; export * as frontmatter from './frontmatter.js';
export * as bases from './bases.js'; export * as bases from './bases.js';
export * as browserControl from './browser-control.js'; export * as browserControl from './browser-control.js';
export * as billing from './billing.js';
export { PrefixLogger }; export { PrefixLogger };

View file

@ -17,6 +17,7 @@ import { UserMessageContent } from './message.js';
import { RowboatApiConfig } from './rowboat-account.js'; import { RowboatApiConfig } from './rowboat-account.js';
import { ZListToolkitsResponse } from './composio.js'; import { ZListToolkitsResponse } from './composio.js';
import { BrowserStateSchema } from './browser-control.js'; import { BrowserStateSchema } from './browser-control.js';
import { BillingInfoSchema } from './billing.js';
// ============================================================================ // ============================================================================
// Runtime Validation Schemas (Single Source of Truth) // Runtime Validation Schemas (Single Source of Truth)
@ -878,15 +879,7 @@ const ipcSchemas = {
// Billing channels // Billing channels
'billing:getInfo': { 'billing:getInfo': {
req: z.null(), req: z.null(),
res: z.object({ res: BillingInfoSchema,
userEmail: z.string().nullable(),
userId: z.string().nullable(),
subscriptionPlan: z.string().nullable(),
subscriptionStatus: z.string().nullable(),
trialExpiresAt: z.string().nullable(),
sanctionedCredits: z.number(),
availableCredits: z.number(),
}),
}, },
} as const; } as const;