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}`);
};
}