diff --git a/apps/x/apps/renderer/src/components/chat-sidebar.tsx b/apps/x/apps/renderer/src/components/chat-sidebar.tsx index a7551757..06680652 100644 --- a/apps/x/apps/renderer/src/components/chat-sidebar.tsx +++ b/apps/x/apps/renderer/src/components/chat-sidebar.tsx @@ -97,7 +97,7 @@ const BILLING_ERROR_PATTERNS = [ { pattern: /not enough credits/i, 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', }, { diff --git a/apps/x/apps/renderer/src/components/settings/account-settings.tsx b/apps/x/apps/renderer/src/components/settings/account-settings.tsx index 1860305d..feee291b 100644 --- a/apps/x/apps/renderer/src/components/settings/account-settings.tsx +++ b/apps/x/apps/renderer/src/components/settings/account-settings.tsx @@ -29,6 +29,7 @@ export function AccountSettings({ dialogOpen }: AccountSettingsProps) { const [connecting, setConnecting] = useState(false) const [appUrl, setAppUrl] = useState(null) const { billing, isLoading: billingLoading } = useBilling(isRowboatConnected) + const hasPaidSubscription = billing?.subscriptionPlan === 'starter' || billing?.subscriptionPlan === 'pro' const checkConnection = useCallback(async () => { try { @@ -178,9 +179,12 @@ export function AccountSettings({ dialogOpen }: AccountSettingsProps) { {!billing.subscriptionPlan && (

Subscribe to access AI features

)} + {billing.subscriptionPlan === 'free' && ( +

Free usage resets daily at 00:00 UTC.

+ )} @@ -203,15 +207,15 @@ export function AccountSettings({ dialogOpen }: AccountSettingsProps) { - {!billing?.subscriptionPlan && ( -

Subscribe to a plan first

+ {!hasPaidSubscription && ( +

Upgrade to a paid plan first

)} diff --git a/apps/x/apps/renderer/src/components/sidebar-content.tsx b/apps/x/apps/renderer/src/components/sidebar-content.tsx index 51aa3940..59ab130c 100644 --- a/apps/x/apps/renderer/src/components/sidebar-content.tsx +++ b/apps/x/apps/renderer/src/components/sidebar-content.tsx @@ -758,7 +758,7 @@ export function SidebarContentPanel({ 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" > - {!billing.subscriptionPlan ? 'Subscribe' : billing.subscriptionPlan === 'starter' ? 'Upgrade' : 'Manage'} + {!billing.subscriptionPlan || billing.subscriptionPlan === 'free' || billing.subscriptionPlan === 'starter' ? 'Upgrade' : 'Manage'} diff --git a/apps/x/apps/renderer/src/hooks/useBilling.ts b/apps/x/apps/renderer/src/hooks/useBilling.ts index 728c6c97..19c001cf 100644 --- a/apps/x/apps/renderer/src/hooks/useBilling.ts +++ b/apps/x/apps/renderer/src/hooks/useBilling.ts @@ -1,14 +1,5 @@ import { useState, useEffect, useCallback } from 'react' - -interface BillingInfo { - userEmail: string | null - userId: string | null - subscriptionPlan: string | null - subscriptionStatus: string | null - trialExpiresAt: string | null - sanctionedCredits: number - availableCredits: number -} +import type { BillingInfo } from '@x/shared/dist/billing.js' export function useBilling(isRowboatConnected: boolean) { const [billing, setBilling] = useState(null) diff --git a/apps/x/packages/core/src/billing/billing.ts b/apps/x/packages/core/src/billing/billing.ts index 2365364d..22bc3d5a 100644 --- a/apps/x/packages/core/src/billing/billing.ts +++ b/apps/x/packages/core/src/billing/billing.ts @@ -1,15 +1,6 @@ import { getAccessToken } from '../auth/tokens.js'; import { API_URL } from '../config/env.js'; - -export interface BillingInfo { - userEmail: string | null; - userId: string | null; - subscriptionPlan: string | null; - subscriptionStatus: string | null; - trialExpiresAt: string | null; - sanctionedCredits: number; - availableCredits: number; -} +import type { BillingInfo, BillingPlan } from '@x/shared/dist/billing.js'; export async function getBillingInfo(): Promise { const accessToken = await getAccessToken(); @@ -25,7 +16,7 @@ export async function getBillingInfo(): Promise { email: string; }; billing: { - plan: string | null; + plan: BillingPlan | null; status: string | null; trialExpiresAt: string | null; usage: { diff --git a/apps/x/packages/shared/src/billing.ts b/apps/x/packages/shared/src/billing.ts new file mode 100644 index 00000000..dca3343a --- /dev/null +++ b/apps/x/packages/shared/src/billing.ts @@ -0,0 +1,15 @@ +import { z } from 'zod'; + +export const BillingPlanSchema = z.enum(['free', 'starter', 'pro']); +export type BillingPlan = z.infer; + +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; diff --git a/apps/x/packages/shared/src/index.ts b/apps/x/packages/shared/src/index.ts index 8d600c5d..02f980a2 100644 --- a/apps/x/packages/shared/src/index.ts +++ b/apps/x/packages/shared/src/index.ts @@ -16,4 +16,5 @@ export * as promptBlock from './prompt-block.js'; export * as frontmatter from './frontmatter.js'; export * as bases from './bases.js'; export * as browserControl from './browser-control.js'; +export * as billing from './billing.js'; export { PrefixLogger }; diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 9ac31ce6..60ef9617 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -17,6 +17,7 @@ import { UserMessageContent } from './message.js'; import { RowboatApiConfig } from './rowboat-account.js'; import { ZListToolkitsResponse } from './composio.js'; import { BrowserStateSchema } from './browser-control.js'; +import { BillingInfoSchema } from './billing.js'; // ============================================================================ // Runtime Validation Schemas (Single Source of Truth) @@ -878,15 +879,7 @@ const ipcSchemas = { // Billing channels 'billing:getInfo': { req: z.null(), - res: z.object({ - 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(), - }), + res: BillingInfoSchema, }, } as const;