@tumbaland/frontend-core 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/entitlementsService.d.ts +58 -0
- package/dist/entitlementsService.js +53 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/plansService.d.ts +51 -0
- package/dist/plansService.js +25 -0
- package/package.json +1 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The caller's own plan, limits and live meter readings.
|
|
3
|
+
*
|
|
4
|
+
* **The vocabulary is duplicated from `@tumbaland/backend-core`; the numbers
|
|
5
|
+
* never are.** A browser bundle cannot import backend-core — it pulls in
|
|
6
|
+
* mongoose — so the meter *keys* are restated here. That is a deliberate line:
|
|
7
|
+
* duplicating the key names costs a rename, while duplicating the limits would
|
|
8
|
+
* recreate exactly the defect Phase 0 removed, where `PlansPage` advertised six
|
|
9
|
+
* ceilings that no service enforced. Every number below arrives from the API at
|
|
10
|
+
* runtime, resolved by the same `normalizeLimits` the middleware enforces from.
|
|
11
|
+
*/
|
|
12
|
+
export type MeterKey = 'storageBytes' | 'aiJobs' | 'trackedTickers' | 'seats';
|
|
13
|
+
export type FeatureKey = 'cleanExport';
|
|
14
|
+
/** Matches `UNLIMITED` in backend-core. Chosen over `Infinity` because it survives JSON. */
|
|
15
|
+
export declare const UNLIMITED = -1;
|
|
16
|
+
export interface PlanLimits {
|
|
17
|
+
meters: Record<MeterKey, number>;
|
|
18
|
+
features: Record<FeatureKey, boolean>;
|
|
19
|
+
/** How far back reads may reach. `-1` means the full history. */
|
|
20
|
+
retentionDays: number;
|
|
21
|
+
}
|
|
22
|
+
export interface Entitlements {
|
|
23
|
+
email: string;
|
|
24
|
+
planCode: string;
|
|
25
|
+
/** Subscription status backing the plan; `null` on the implicit free plan. */
|
|
26
|
+
status: string | null;
|
|
27
|
+
limits: PlanLimits;
|
|
28
|
+
/** Cheapest active plan priced above the current one, for an upgrade CTA. */
|
|
29
|
+
upgradeTo?: string;
|
|
30
|
+
/** Granted by an admin rather than bought — render as "Complimentary". */
|
|
31
|
+
isComp?: boolean;
|
|
32
|
+
hasOverride?: boolean;
|
|
33
|
+
/** Live meter readings, same keys as `limits.meters`. */
|
|
34
|
+
usage: Record<MeterKey, number>;
|
|
35
|
+
}
|
|
36
|
+
export interface EntitlementsServiceConfig {
|
|
37
|
+
/** Read fresh at call time (config may not be resolved yet at module load). */
|
|
38
|
+
getPaymentApiUrl: () => string;
|
|
39
|
+
onUnauthorized?: () => void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Builds a per-front entitlements service.
|
|
43
|
+
*
|
|
44
|
+
* export const { getMyEntitlements } = createEntitlementsService({
|
|
45
|
+
* getPaymentApiUrl: () => getGlobalConfig().PAYMENT_API_URL!
|
|
46
|
+
* });
|
|
47
|
+
*/
|
|
48
|
+
export declare function createEntitlementsService(config: EntitlementsServiceConfig): {
|
|
49
|
+
getMyEntitlements(): Promise<Entitlements>;
|
|
50
|
+
};
|
|
51
|
+
export type EntitlementsService = ReturnType<typeof createEntitlementsService>;
|
|
52
|
+
/** True when a meter has no ceiling. */
|
|
53
|
+
export declare function isUnlimited(limit: number): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Share of a meter consumed, 0–1, clamped. Unlimited meters report 0 — there is
|
|
56
|
+
* no ceiling to be a fraction of, and reporting 1 would paint them as full.
|
|
57
|
+
*/
|
|
58
|
+
export declare function usageRatio(used: number, limit: number): number;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createApiClient } from './apiClient';
|
|
2
|
+
/** Matches `UNLIMITED` in backend-core. Chosen over `Infinity` because it survives JSON. */
|
|
3
|
+
export const UNLIMITED = -1;
|
|
4
|
+
const ZERO_USAGE = {
|
|
5
|
+
storageBytes: 0,
|
|
6
|
+
aiJobs: 0,
|
|
7
|
+
trackedTickers: 0,
|
|
8
|
+
seats: 0
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Builds a per-front entitlements service.
|
|
12
|
+
*
|
|
13
|
+
* export const { getMyEntitlements } = createEntitlementsService({
|
|
14
|
+
* getPaymentApiUrl: () => getGlobalConfig().PAYMENT_API_URL!
|
|
15
|
+
* });
|
|
16
|
+
*/
|
|
17
|
+
export function createEntitlementsService(config) {
|
|
18
|
+
const client = createApiClient({
|
|
19
|
+
baseUrl: () => config.getPaymentApiUrl(),
|
|
20
|
+
onUnauthorized: config.onUnauthorized
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
async getMyEntitlements() {
|
|
24
|
+
const body = await client.get('/api/subscriptions/me/entitlements');
|
|
25
|
+
// Reject rather than return a half-built object. Without `limits` there
|
|
26
|
+
// is no ceiling to meter against, and handing back a truthy
|
|
27
|
+
// `Entitlements` whose `limits` is undefined pushes the crash into every
|
|
28
|
+
// call site's property access instead of failing here, once.
|
|
29
|
+
if (!body?.limits?.meters) {
|
|
30
|
+
throw new Error('Entitlements response is missing limits');
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
...body,
|
|
34
|
+
// A meter with no counter yet is simply absent from the snapshot, which
|
|
35
|
+
// would otherwise render as `undefined of 1 GB`.
|
|
36
|
+
usage: { ...ZERO_USAGE, ...(body.usage ?? {}) }
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** True when a meter has no ceiling. */
|
|
42
|
+
export function isUnlimited(limit) {
|
|
43
|
+
return limit === UNLIMITED || limit < 0;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Share of a meter consumed, 0–1, clamped. Unlimited meters report 0 — there is
|
|
47
|
+
* no ceiling to be a fraction of, and reporting 1 would paint them as full.
|
|
48
|
+
*/
|
|
49
|
+
export function usageRatio(used, limit) {
|
|
50
|
+
if (isUnlimited(limit) || limit === 0)
|
|
51
|
+
return 0;
|
|
52
|
+
return Math.min(1, Math.max(0, used / limit));
|
|
53
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ export { createAuthService, createAppAuthService } from './authService';
|
|
|
4
4
|
export type { AuthServiceConfig, AuthService, AppAuthConfig } from './authService';
|
|
5
5
|
export { createGroupService } from './groupService';
|
|
6
6
|
export type { GroupServiceConfig, GroupService } from './groupService';
|
|
7
|
+
export { createEntitlementsService, isUnlimited, usageRatio, UNLIMITED } from './entitlementsService';
|
|
8
|
+
export type { EntitlementsServiceConfig, EntitlementsService, Entitlements, PlanLimits, MeterKey, FeatureKey } from './entitlementsService';
|
|
9
|
+
export { createPlansService } from './plansService';
|
|
10
|
+
export type { CatalogPlan, PlansServiceConfig, PlansService } from './plansService';
|
|
7
11
|
export type { User, AuthResponse, Group, ApiResponse } from './types';
|
|
8
12
|
export { TENANT_STORAGE_KEY, clearSessionScopedStorage } from './sessionStorage';
|
|
9
13
|
export { initMonitoring, captureException, captureMessage, setUser, setTag, startTransaction, recordMetric, initWebVitals, flushLogs, initConsoleInterception, generateCorrelationId, getCorrelationId, getSessionId, logger } from './monitoring';
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
export { createApiClient, ApiError } from './apiClient';
|
|
3
3
|
export { createAuthService, createAppAuthService } from './authService';
|
|
4
4
|
export { createGroupService } from './groupService';
|
|
5
|
+
export { createEntitlementsService, isUnlimited, usageRatio, UNLIMITED } from './entitlementsService';
|
|
6
|
+
export { createPlansService } from './plansService';
|
|
5
7
|
export { TENANT_STORAGE_KEY, clearSessionScopedStorage } from './sessionStorage';
|
|
6
8
|
// Monitoring: headless logging, error capture, correlation IDs, and web-vitals
|
|
7
9
|
export { initMonitoring, captureException, captureMessage, setUser, setTag, startTransaction, recordMetric, initWebVitals, flushLogs, initConsoleInterception, generateCorrelationId, getCorrelationId, getSessionId, logger } from './monitoring';
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { PlanLimits } from './entitlementsService';
|
|
2
|
+
/**
|
|
3
|
+
* The public plan catalog, as the pricing page renders it.
|
|
4
|
+
*
|
|
5
|
+
* **The numbers are never duplicated here.** `effectiveLimits` arrives from the
|
|
6
|
+
* API already resolved by the same `normalizeLimits` the entitlement middleware
|
|
7
|
+
* enforces from, so the ceiling on the card and the ceiling that produces a 402
|
|
8
|
+
* are the same value read from the same place. A hardcoded pricing table is the
|
|
9
|
+
* exact defect Phase 0 removed — `PlansPage` advertised six limits no service
|
|
10
|
+
* enforced — and re-creating it at the moment money changes hands would attach
|
|
11
|
+
* a payment to the claim.
|
|
12
|
+
*
|
|
13
|
+
* What a front *may* own is copy: a plan's tagline and which card is
|
|
14
|
+
* highlighted are presentation, not entitlement.
|
|
15
|
+
*/
|
|
16
|
+
export interface CatalogPlan {
|
|
17
|
+
code: string;
|
|
18
|
+
name: string;
|
|
19
|
+
/** Admin-editable tagline. Absent on plans nobody has written copy for. */
|
|
20
|
+
description?: string;
|
|
21
|
+
/** Cents, so there is never a float in a price. */
|
|
22
|
+
priceMonthlyCents: number;
|
|
23
|
+
priceYearlyCents?: number;
|
|
24
|
+
/** Marketing bullets. Deliberately unused by the limit rows — see above. */
|
|
25
|
+
features?: string[];
|
|
26
|
+
isPublic?: boolean;
|
|
27
|
+
/** Whether the plan can be bought at all; a priced plan without one returns 503 at checkout. */
|
|
28
|
+
stripePriceId?: string;
|
|
29
|
+
/** Limits after code defaults fill whatever the plan document leaves unset. */
|
|
30
|
+
effectiveLimits: PlanLimits;
|
|
31
|
+
}
|
|
32
|
+
export interface PlansServiceConfig {
|
|
33
|
+
/** Read fresh at call time (config may not be resolved yet at module load). */
|
|
34
|
+
getPaymentApiUrl: () => string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Builds a per-front plans service.
|
|
38
|
+
*
|
|
39
|
+
* export const { getPlans } = createPlansService({
|
|
40
|
+
* getPaymentApiUrl: () => getGlobalConfig().PAYMENT_API_URL!
|
|
41
|
+
* });
|
|
42
|
+
*
|
|
43
|
+
* `GET /api/plans` is optionally authenticated: a signed-out visitor gets the
|
|
44
|
+
* public catalog, and a signed-in one additionally gets the hidden plan they
|
|
45
|
+
* are actually on. No `onUnauthorized` redirect, therefore — the landing page
|
|
46
|
+
* is reachable without an account and must stay that way.
|
|
47
|
+
*/
|
|
48
|
+
export declare function createPlansService(config: PlansServiceConfig): {
|
|
49
|
+
getPlans(): Promise<CatalogPlan[]>;
|
|
50
|
+
};
|
|
51
|
+
export type PlansService = ReturnType<typeof createPlansService>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createApiClient } from './apiClient';
|
|
2
|
+
/**
|
|
3
|
+
* Builds a per-front plans service.
|
|
4
|
+
*
|
|
5
|
+
* export const { getPlans } = createPlansService({
|
|
6
|
+
* getPaymentApiUrl: () => getGlobalConfig().PAYMENT_API_URL!
|
|
7
|
+
* });
|
|
8
|
+
*
|
|
9
|
+
* `GET /api/plans` is optionally authenticated: a signed-out visitor gets the
|
|
10
|
+
* public catalog, and a signed-in one additionally gets the hidden plan they
|
|
11
|
+
* are actually on. No `onUnauthorized` redirect, therefore — the landing page
|
|
12
|
+
* is reachable without an account and must stay that way.
|
|
13
|
+
*/
|
|
14
|
+
export function createPlansService(config) {
|
|
15
|
+
const client = createApiClient({ baseUrl: () => config.getPaymentApiUrl() });
|
|
16
|
+
return {
|
|
17
|
+
async getPlans() {
|
|
18
|
+
const body = await client.get('/api/plans');
|
|
19
|
+
const plans = body?.plans ?? [];
|
|
20
|
+
// A plan without resolved limits cannot be rendered honestly — every line
|
|
21
|
+
// on its card is a limit — so drop it rather than draw a card with holes.
|
|
22
|
+
return plans.filter(plan => Boolean(plan?.effectiveLimits?.meters));
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|