mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-22 18:45:19 +02:00
Update Electron billing UI for free plan
This commit is contained in:
parent
d586f6bd8a
commit
af618155e1
8 changed files with 31 additions and 36 deletions
|
|
@ -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',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
15
apps/x/packages/shared/src/billing.ts
Normal file
15
apps/x/packages/shared/src/billing.ts
Normal 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>;
|
||||||
|
|
@ -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 };
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue