learning_ai_clock/web/src/lib/billing-client.ts
saravanakumardb1 83e54c29be fix(web): append /api to baseUrl for shared clients
getBaseUrl() returns 'http://localhost:4003' without /api suffix.
Shared feature-flag-client and subscription-client expect the API
prefix in the URL. Without this fix, requests hit /flags/poll and
/subscriptions/me instead of /api/flags/poll and /api/subscriptions/me.
2026-03-19 17:08:44 -07:00

108 lines
3.4 KiB
TypeScript

/**
* Billing / Subscription client — thin wrapper over @bytelyst/subscription-client.
*
* Delegates subscription operations to the shared package.
* Keeps ChronoMind-specific usage API as a local fetch call.
* Client-side only — uses the stored auth token for authorization.
*/
import { createSubscriptionClient } from '@bytelyst/subscription-client';
import type { SubscriptionDoc } from '@bytelyst/subscription-client';
import { PRODUCT_ID, getBaseUrl, getAuthClient } from './auth-api';
// ── Shared client ──────────────────────────────────────────────────
let _client: ReturnType<typeof createSubscriptionClient> | null = null;
function getClient() {
if (!_client) {
_client = createSubscriptionClient({
baseUrl: `${getBaseUrl()}/api`,
productId: PRODUCT_ID,
userId: 'me',
getAccessToken: () => getAuthClient().getAccessToken() ?? '',
});
}
return _client;
}
// ── Types (backwards-compatible) ───────────────────────────────────
export interface Subscription {
id: string;
productId: string;
userId: string;
plan: 'free' | 'pro';
status: 'active' | 'cancelled' | 'past_due' | 'trialing';
currentPeriodStart: string;
currentPeriodEnd: string;
cancelAtPeriodEnd: boolean;
stripeCustomerId?: string;
stripeSubscriptionId?: string;
createdAt: string;
updatedAt: string;
}
function toSubscription(doc: SubscriptionDoc): Subscription {
return {
id: doc.id,
productId: doc.productId ?? PRODUCT_ID,
userId: doc.userId,
plan: doc.plan as Subscription['plan'],
status: doc.status as Subscription['status'],
currentPeriodStart: doc.currentPeriodStart,
currentPeriodEnd: doc.currentPeriodEnd,
cancelAtPeriodEnd: doc.cancelAtPeriodEnd,
stripeCustomerId: doc.stripeCustomerId,
stripeSubscriptionId: doc.stripeSubscriptionId,
createdAt: doc.createdAt,
updatedAt: doc.updatedAt,
};
}
// ── Subscription API ───────────────────────────────────────────────
export async function getMySubscription(_userId?: string): Promise<Subscription | null> {
try {
const doc = await getClient().getMySubscription();
return doc ? toSubscription(doc) : null;
} catch {
return null;
}
}
export async function cancelSubscription(_userId?: string): Promise<Subscription | null> {
try {
const doc = await getClient().cancelSubscription();
return toSubscription(doc);
} catch {
return null;
}
}
// ── Usage API (ChronoMind-specific, not in shared client) ──────────
export interface UsageSummary {
timersCreated: number;
routinesCompleted: number;
focusMinutes: number;
}
export async function getUsageSummary(userId: string, days = 30): Promise<UsageSummary | null> {
try {
const token = getAuthClient().getAccessToken();
const res = await fetch(`${getBaseUrl()}/usage/summary?userId=${userId}&days=${days}`, {
headers: {
'Content-Type': 'application/json',
'x-product-id': PRODUCT_ID,
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
});
if (!res.ok) return null;
const data = await res.json() as { summary: UsageSummary };
return data.summary;
} catch {
return null;
}
}