feat(desktop): add billing frontend — plans, subscription, payment flow

Sprint 1: Desktop 计费闭环

- Add 7 billing types to saas-types.ts (BillingPlan, Subscription, UsageQuota, etc.)
- Add 6 billing API methods to saas-billing.ts (listPlans, getSubscription, createPayment, etc.)
- Extend saas-client.ts with interface merging for billing methods
- Extend saasStore with billing state/actions (plans, subscription, payment polling)
- Create PricingPage component with plan cards, usage bars, and checkout modal
- Add billing page entry in SettingsLayout (CreditCard icon + route)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
iven
2026-04-04 10:48:33 +08:00
parent be0a78a523
commit eac1d9449e
6 changed files with 670 additions and 3 deletions

View File

@@ -1,10 +1,20 @@
/**
* SaaS Billing Methods — Mixin
*
* Installs billing-related methods (usage increment, quota check) onto
* SaaSClient.prototype. Uses the same mixin pattern as saas-telemetry.ts.
* Installs billing-related methods onto SaaSClient.prototype.
* Covers usage increment (fire-and-forget) and full billing CRUD
* (plans, subscription, usage, payments).
*/
import type {
BillingPlan,
SubscriptionInfo,
UsageQuota,
CreatePaymentRequest,
PaymentResult,
PaymentStatus,
} from './saas-types';
export interface UsageIncrementResult {
dimension: string;
incremented: number;
@@ -18,9 +28,13 @@ export interface UsageIncrementResult {
};
}
type RequestFn = <T>(method: string, path: string, body?: unknown) => Promise<T>;
export function installBillingMethods(ClientClass: { prototype: any }): void {
const proto = ClientClass.prototype;
// --- Usage Increment ---
/**
* Report a usage increment for a specific dimension.
*
@@ -52,9 +66,61 @@ export function installBillingMethods(ClientClass: { prototype: any }): void {
count: number = 1,
): void {
this.incrementUsageDimension(dimension, count).catch((err: unknown) => {
// Non-fatal: billing reporting failure must never block user operations
const msg = err instanceof Error ? err.message : String(err);
console.warn(`[Billing] Failed to report ${dimension} usage (+${count}): ${msg}`);
});
};
// --- Plans ---
/** List all active billing plans (public, no auth required) */
proto.listPlans = async function (
this: { request: RequestFn },
): Promise<BillingPlan[]> {
return this.request<BillingPlan[]>('GET', '/api/v1/billing/plans');
};
/** Get a single plan by ID */
proto.getPlan = async function (
this: { request: RequestFn },
planId: string,
): Promise<BillingPlan> {
return this.request<BillingPlan>('GET', `/api/v1/billing/plans/${planId}`);
};
// --- Subscription ---
/** Get current subscription info (plan + subscription + usage) */
proto.getSubscription = async function (
this: { request: RequestFn },
): Promise<SubscriptionInfo> {
return this.request<SubscriptionInfo>('GET', '/api/v1/billing/subscription');
};
// --- Usage ---
/** Get current month's usage quota */
proto.getUsage = async function (
this: { request: RequestFn },
): Promise<UsageQuota> {
return this.request<UsageQuota>('GET', '/api/v1/billing/usage');
};
// --- Payments ---
/** Create a payment order for a plan upgrade */
proto.createPayment = async function (
this: { request: RequestFn },
data: CreatePaymentRequest,
): Promise<PaymentResult> {
return this.request<PaymentResult>('POST', '/api/v1/billing/payments', data);
};
/** Check payment status by payment ID */
proto.getPaymentStatus = async function (
this: { request: RequestFn },
paymentId: string,
): Promise<PaymentStatus> {
return this.request<PaymentStatus>('GET', `/api/v1/billing/payments/${paymentId}`);
};
}

View File

@@ -115,6 +115,18 @@ import { installRelayMethods } from './saas-relay';
import { installPromptMethods } from './saas-prompt';
import { installTelemetryMethods } from './saas-telemetry';
import { installBillingMethods } from './saas-billing';
export type { UsageIncrementResult } from './saas-billing';
// Re-export billing types for convenience
export type {
BillingPlan,
Subscription,
UsageQuota,
SubscriptionInfo,
CreatePaymentRequest,
PaymentResult,
PaymentStatus,
} from './saas-types';
// === Client Implementation ===
@@ -448,6 +460,7 @@ installRelayMethods(SaaSClient);
installPromptMethods(SaaSClient);
installTelemetryMethods(SaaSClient);
installBillingMethods(SaaSClient);
export { installBillingMethods };
// === API Method Type Declarations ===
// These methods are installed at runtime by installXxxMethods() in saas-*.ts.
@@ -506,6 +519,12 @@ export interface SaaSClient {
// --- Billing (saas-billing.ts) ---
incrementUsageDimension(dimension: string, count?: number): Promise<import('./saas-billing').UsageIncrementResult>;
reportUsageFireAndForget(dimension: string, count?: number): void;
listPlans(): Promise<import('./saas-types').BillingPlan[]>;
getPlan(planId: string): Promise<import('./saas-types').BillingPlan>;
getSubscription(): Promise<import('./saas-types').SubscriptionInfo>;
getUsage(): Promise<import('./saas-types').UsageQuota>;
createPayment(data: import('./saas-types').CreatePaymentRequest): Promise<import('./saas-types').PaymentResult>;
getPaymentStatus(paymentId: string): Promise<import('./saas-types').PaymentStatus>;
}
// === Singleton ===

View File

@@ -468,6 +468,91 @@ export interface AssignTemplateRequest {
template_id: string;
}
// === Billing Types ===
/** Subscription plan definition */
export interface BillingPlan {
id: string;
name: string;
display_name: string;
description: string | null;
price_cents: number;
currency: string;
interval: string;
features: Record<string, unknown>;
limits: Record<string, unknown>;
is_default: boolean;
sort_order: number;
status: string;
created_at: string;
updated_at: string;
}
/** User subscription record */
export interface Subscription {
id: string;
account_id: string;
plan_id: string;
status: string;
current_period_start: string;
current_period_end: string;
trial_end: string | null;
canceled_at: string | null;
cancel_at_period_end: boolean;
created_at: string;
updated_at: string;
}
/** Monthly usage quota tracking */
export interface UsageQuota {
id: string;
account_id: string;
period_start: string;
period_end: string;
input_tokens: number;
output_tokens: number;
relay_requests: number;
hand_executions: number;
pipeline_runs: number;
max_input_tokens: number | null;
max_output_tokens: number | null;
max_relay_requests: number | null;
max_hand_executions: number | null;
max_pipeline_runs: number | null;
created_at: string;
updated_at: string;
}
/** Combined subscription info (plan + subscription + usage) */
export interface SubscriptionInfo {
plan: BillingPlan;
subscription: Subscription | null;
usage: UsageQuota;
}
/** Payment creation request */
export interface CreatePaymentRequest {
plan_id: string;
payment_method: 'alipay' | 'wechat';
}
/** Payment creation result */
export interface PaymentResult {
payment_id: string;
trade_no: string;
pay_url: string;
amount_cents: number;
}
/** Payment status query result */
export interface PaymentStatus {
id: string;
method: string;
amount_cents: number;
currency: string;
status: string;
}
/** Agent configuration derived from a template.
* capabilities are merged into tools (no separate field). */
export interface AgentConfigFromTemplate {