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:
@@ -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}`);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 ===
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user