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.
108 lines
3.4 KiB
TypeScript
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;
|
|
}
|
|
}
|