From 75ffbc781c1040fc73dd94bb8d2e8acb93e89d63 Mon Sep 17 00:00:00 2001 From: tusharmagar Date: Tue, 7 Apr 2026 21:51:17 +0530 Subject: [PATCH] Add billing error handling and UI updates - Introduced billing error patterns to match specific error messages and display appropriate user prompts in the ChatSidebar. - Enhanced SidebarContentPanel and AccountSettings components to reflect subscription status, including trial expiration details. - Updated button actions to direct users to the app URL for subscription management and upgrades. - Added a new Payment section in AccountSettings for managing invoices and payment methods, with conditional rendering based on subscription status. --- .../renderer/src/components/chat-sidebar.tsx | 62 +++++++++++++++++++ .../components/settings/account-settings.tsx | 48 ++++++++++++-- .../src/components/sidebar-content.tsx | 22 +++++-- apps/x/apps/renderer/src/hooks/useBilling.ts | 1 + apps/x/packages/core/src/billing/billing.ts | 3 + apps/x/packages/shared/src/ipc.ts | 1 + 6 files changed, 126 insertions(+), 11 deletions(-) diff --git a/apps/x/apps/renderer/src/components/chat-sidebar.tsx b/apps/x/apps/renderer/src/components/chat-sidebar.tsx index 64b1e843..f94c94ba 100644 --- a/apps/x/apps/renderer/src/components/chat-sidebar.tsx +++ b/apps/x/apps/renderer/src/components/chat-sidebar.tsx @@ -49,6 +49,54 @@ import { const streamdownComponents = { pre: MarkdownPreOverride } +/* ─── Billing error helpers ─── */ + +const BILLING_ERROR_PATTERNS = [ + { + pattern: /upgrade required/i, + title: 'A subscription is required', + subtitle: 'Get started with a plan to access AI features in Rowboat.', + cta: 'Subscribe', + }, + { + 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.', + cta: 'Upgrade plan', + }, + { + pattern: /subscription not active/i, + title: 'Your subscription is inactive', + subtitle: 'Reactivate your subscription to continue using AI features.', + cta: 'Reactivate', + }, +] as const + +function matchBillingError(message: string) { + return BILLING_ERROR_PATTERNS.find(({ pattern }) => pattern.test(message)) ?? null +} + +function BillingErrorCTA({ label }: { label: string }) { + const [appUrl, setAppUrl] = useState(null) + + useEffect(() => { + window.ipc.invoke('account:getRowboat', null) + .then((account: any) => setAppUrl(account.config?.appUrl ?? null)) + .catch(() => {}) + }, []) + + if (!appUrl) return null + + return ( + + ) +} + const MIN_WIDTH = 360 const MAX_WIDTH = 1600 const MIN_MAIN_PANE_WIDTH = 420 @@ -378,6 +426,20 @@ export function ChatSidebar({ } if (isErrorMessage(item)) { + const billingError = matchBillingError(item.message) + if (billingError) { + return ( + + +
+

{billingError.title}

+

{billingError.subtitle}

+ +
+
+
+ ) + } return ( 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 e4d9b93f..1860305d 100644 --- a/apps/x/apps/renderer/src/components/settings/account-settings.tsx +++ b/apps/x/apps/renderer/src/components/settings/account-settings.tsx @@ -1,7 +1,7 @@ "use client" import { useState, useEffect, useCallback } from "react" -import { Loader2, User, CreditCard, LogOut } from "lucide-react" +import { Loader2, User, CreditCard, LogOut, ExternalLink } from "lucide-react" import { Button } from "@/components/ui/button" import { AlertDialog, @@ -162,13 +162,25 @@ export function AccountSettings({ dialogOpen }: AccountSettingsProps) {
-

{billing.subscriptionPlan ?? 'Free'} Plan

- {billing.subscriptionStatus && ( +

+ {billing.subscriptionPlan ? `${billing.subscriptionPlan} Plan` : 'No Plan'} +

+ {billing.subscriptionStatus === 'trialing' && billing.trialExpiresAt ? (() => { + const days = Math.max(0, Math.ceil((new Date(billing.trialExpiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24))) + return ( +

+ Trial · {days === 0 ? 'expires today' : days === 1 ? '1 day left' : `${days} days left`} +

+ ) + })() : billing.subscriptionStatus ? (

{billing.subscriptionStatus}

+ ) : null} + {!billing.subscriptionPlan && ( +

Subscribe to access AI features

)}
-
@@ -179,6 +191,32 @@ export function AccountSettings({ dialogOpen }: AccountSettingsProps) { + {/* Payment Section */} +
+
+ +

Payment

+
+

+ Manage invoices, payment methods, and billing details. +

+ + {!billing?.subscriptionPlan && ( +

Subscribe to a plan first

+ )} +
+ + + {/* Log Out Section */}
diff --git a/apps/x/apps/renderer/src/components/sidebar-content.tsx b/apps/x/apps/renderer/src/components/sidebar-content.tsx index 43770847..7e41783e 100644 --- a/apps/x/apps/renderer/src/components/sidebar-content.tsx +++ b/apps/x/apps/renderer/src/components/sidebar-content.tsx @@ -515,14 +515,24 @@ export function SidebarContentPanel({ {isRowboatConnected && billing ? (
- - {billing.subscriptionPlan ?? 'Free'} plan - +
+ + {billing.subscriptionPlan ? `${billing.subscriptionPlan} plan` : 'No plan'} + + {billing.subscriptionStatus === 'trialing' && billing.trialExpiresAt && (() => { + const days = Math.max(0, Math.ceil((new Date(billing.trialExpiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24))) + return ( +

+ {days === 0 ? 'Trial expires today' : days === 1 ? '1 day left' : `${days} days left`} +

+ ) + })()} +
diff --git a/apps/x/apps/renderer/src/hooks/useBilling.ts b/apps/x/apps/renderer/src/hooks/useBilling.ts index bf56980b..728c6c97 100644 --- a/apps/x/apps/renderer/src/hooks/useBilling.ts +++ b/apps/x/apps/renderer/src/hooks/useBilling.ts @@ -5,6 +5,7 @@ interface BillingInfo { userId: string | null subscriptionPlan: string | null subscriptionStatus: string | null + trialExpiresAt: string | null sanctionedCredits: number availableCredits: number } diff --git a/apps/x/packages/core/src/billing/billing.ts b/apps/x/packages/core/src/billing/billing.ts index b3011130..2365364d 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; + trialExpiresAt: string | null; sanctionedCredits: number; availableCredits: number; } @@ -26,6 +27,7 @@ export async function getBillingInfo(): Promise { billing: { plan: string | null; status: string | null; + trialExpiresAt: string | 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, + trialExpiresAt: body.billing.trialExpiresAt ?? 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 daf49b95..021db404 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -566,6 +566,7 @@ const ipcSchemas = { userId: z.string().nullable(), subscriptionPlan: z.string().nullable(), subscriptionStatus: z.string().nullable(), + trialExpiresAt: z.string().nullable(), sanctionedCredits: z.number(), availableCredits: z.number(), }),