@lukoweb/apitogo-plugin-dev-portal-billing 0.1.40 → 0.1.41
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/index.mjs +4 -1
- package/package.json +7 -9
- package/src/DevPortalBillingContext.tsx +19 -0
- package/src/DevPortalBillingPlugin.test.tsx +29 -0
- package/src/DevPortalBillingPlugin.tsx +87 -0
- package/src/DevPortalBillingWrapper.tsx +18 -0
- package/src/components/PricingCard.test.tsx +47 -0
- package/src/components/PricingCard.tsx +142 -0
- package/src/components/RedirectPage.tsx +47 -0
- package/src/env.d.ts +2 -0
- package/src/hooks/useCancelSubscription.ts +44 -0
- package/src/hooks/useCredentials.ts +39 -0
- package/src/hooks/useDevPortalApiBaseUrl.ts +33 -0
- package/src/hooks/useMySubscriptions.ts +31 -0
- package/src/hooks/usePlans.ts +35 -0
- package/src/index.ts +8 -0
- package/src/pages/AccountPlansPage.tsx +106 -0
- package/src/pages/CheckoutPage.tsx +127 -0
- package/src/pages/CredentialsPage.tsx +145 -0
- package/src/pages/PricingPage.tsx +87 -0
- package/src/pages/SubscriptionsPage.tsx +129 -0
- package/src/paths.ts +4 -0
- package/src/types.ts +50 -0
- package/src/utils/dev-portal-api.test.ts +89 -0
- package/src/utils/dev-portal-api.ts +156 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { DevPortalPlan } from "../types.js";
|
|
3
|
+
import {
|
|
4
|
+
formatPlanPrice,
|
|
5
|
+
isActiveSubscription,
|
|
6
|
+
parseMockPlans,
|
|
7
|
+
summarizeLimits,
|
|
8
|
+
} from "./dev-portal-api.js";
|
|
9
|
+
|
|
10
|
+
describe("parseMockPlans", () => {
|
|
11
|
+
it("returns empty catalog when env is unset", () => {
|
|
12
|
+
expect(parseMockPlans()).toEqual({ plans: [] });
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("formatPlanPrice", () => {
|
|
17
|
+
it("formats free plans", () => {
|
|
18
|
+
const plan: DevPortalPlan = {
|
|
19
|
+
sku: "free",
|
|
20
|
+
displayName: "Free",
|
|
21
|
+
limitsJson: null,
|
|
22
|
+
isFree: true,
|
|
23
|
+
priceAmount: 0,
|
|
24
|
+
priceCurrency: "usd",
|
|
25
|
+
billingPeriod: "monthly",
|
|
26
|
+
};
|
|
27
|
+
expect(formatPlanPrice(plan)).toBe("Free");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("formats paid monthly plans", () => {
|
|
31
|
+
const plan: DevPortalPlan = {
|
|
32
|
+
sku: "pro",
|
|
33
|
+
displayName: "Pro",
|
|
34
|
+
limitsJson: null,
|
|
35
|
+
isFree: false,
|
|
36
|
+
priceAmount: 29,
|
|
37
|
+
priceCurrency: "usd",
|
|
38
|
+
billingPeriod: "monthly",
|
|
39
|
+
};
|
|
40
|
+
expect(formatPlanPrice(plan)).toMatch(/\$29(\.00)?\/mo/);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("summarizeLimits", () => {
|
|
45
|
+
it("formats APItoGo rateLimit as requests per minute", () => {
|
|
46
|
+
expect(
|
|
47
|
+
summarizeLimits(
|
|
48
|
+
JSON.stringify({
|
|
49
|
+
rateLimit: { calls: 3, renewalPeriodSeconds: 60 },
|
|
50
|
+
quota: null,
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
).toBe("3 requests per minute");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("formats ultimate-tier rate limits", () => {
|
|
57
|
+
expect(
|
|
58
|
+
summarizeLimits(
|
|
59
|
+
JSON.stringify({
|
|
60
|
+
rateLimit: { calls: 10, renewalPeriodSeconds: 60 },
|
|
61
|
+
quota: null,
|
|
62
|
+
}),
|
|
63
|
+
),
|
|
64
|
+
).toBe("10 requests per minute");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("formats non-minute renewal windows", () => {
|
|
68
|
+
expect(
|
|
69
|
+
summarizeLimits(
|
|
70
|
+
JSON.stringify({
|
|
71
|
+
rateLimit: { calls: 5, renewalPeriodSeconds: 120 },
|
|
72
|
+
}),
|
|
73
|
+
),
|
|
74
|
+
).toBe("5 requests per 120 seconds");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("summarizes flat JSON limits", () => {
|
|
78
|
+
expect(
|
|
79
|
+
summarizeLimits(JSON.stringify({ requestsPerMonth: 1000 })),
|
|
80
|
+
).toContain("requestsPerMonth: 1000");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("isActiveSubscription", () => {
|
|
85
|
+
it("detects active status", () => {
|
|
86
|
+
expect(isActiveSubscription("Active")).toBe(true);
|
|
87
|
+
expect(isActiveSubscription("canceled")).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { DevPortalPlan, DevPortalPlanCatalog } from "../types.js";
|
|
2
|
+
|
|
3
|
+
export const DEV_PORTAL_PLACEHOLDER_API_URL =
|
|
4
|
+
"https://dp-example.azurecontainerapps.io";
|
|
5
|
+
|
|
6
|
+
export function resolveDevPortalApiBaseUrl(
|
|
7
|
+
configuredUrl?: string,
|
|
8
|
+
): string | undefined {
|
|
9
|
+
const fromConfig = configuredUrl?.trim();
|
|
10
|
+
if (fromConfig) {
|
|
11
|
+
return fromConfig.replace(/\/+$/, "");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const envUrl =
|
|
15
|
+
typeof VITE_APITOGO_DEV_PORTAL_API_URL === "string"
|
|
16
|
+
? VITE_APITOGO_DEV_PORTAL_API_URL.trim()
|
|
17
|
+
: "";
|
|
18
|
+
|
|
19
|
+
return envUrl ? envUrl.replace(/\/+$/, "") : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isPlaceholderDevPortalApiUrl(
|
|
23
|
+
apiBaseUrl: string | undefined,
|
|
24
|
+
): boolean {
|
|
25
|
+
if (!apiBaseUrl) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
apiBaseUrl === DEV_PORTAL_PLACEHOLDER_API_URL ||
|
|
31
|
+
apiBaseUrl.includes("dp-example.azurecontainerapps.io")
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function parseMockPlans(): DevPortalPlanCatalog {
|
|
36
|
+
const raw =
|
|
37
|
+
typeof VITE_APITOGO_DEV_PORTAL_MOCK_PLANS === "string"
|
|
38
|
+
? VITE_APITOGO_DEV_PORTAL_MOCK_PLANS.trim()
|
|
39
|
+
: "";
|
|
40
|
+
|
|
41
|
+
if (!raw) {
|
|
42
|
+
return { plans: [] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(raw) as Array<{
|
|
47
|
+
sku: string;
|
|
48
|
+
displayName: string;
|
|
49
|
+
isFree?: boolean;
|
|
50
|
+
priceAmount?: number;
|
|
51
|
+
priceCurrency?: string;
|
|
52
|
+
billingPeriod?: string;
|
|
53
|
+
limitsJson?: string | null;
|
|
54
|
+
}>;
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
plans: parsed.map((plan) => ({
|
|
58
|
+
sku: plan.sku,
|
|
59
|
+
displayName: plan.displayName,
|
|
60
|
+
isFree: plan.isFree ?? false,
|
|
61
|
+
priceAmount: plan.priceAmount ?? 0,
|
|
62
|
+
priceCurrency: plan.priceCurrency ?? "usd",
|
|
63
|
+
billingPeriod: plan.billingPeriod ?? "monthly",
|
|
64
|
+
limitsJson: plan.limitsJson ?? null,
|
|
65
|
+
})),
|
|
66
|
+
};
|
|
67
|
+
} catch {
|
|
68
|
+
return { plans: [] };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function formatPlanPrice(plan: DevPortalPlan): string {
|
|
73
|
+
if (plan.isFree || plan.priceAmount <= 0) {
|
|
74
|
+
return "Free";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const amount = new Intl.NumberFormat(undefined, {
|
|
78
|
+
style: "currency",
|
|
79
|
+
currency: plan.priceCurrency.toUpperCase(),
|
|
80
|
+
minimumFractionDigits: plan.priceAmount % 1 === 0 ? 0 : 2,
|
|
81
|
+
}).format(plan.priceAmount);
|
|
82
|
+
|
|
83
|
+
const period = plan.billingPeriod.toLowerCase();
|
|
84
|
+
if (period === "monthly" || period === "month") {
|
|
85
|
+
return `${amount}/mo`;
|
|
86
|
+
}
|
|
87
|
+
if (period === "annual" || period === "year") {
|
|
88
|
+
return `${amount}/yr`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return amount;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function formatBillingPeriod(period: string): string {
|
|
95
|
+
const normalized = period.toLowerCase();
|
|
96
|
+
if (normalized === "month" || normalized === "monthly") {
|
|
97
|
+
return "Monthly";
|
|
98
|
+
}
|
|
99
|
+
if (normalized === "year" || normalized === "annual") {
|
|
100
|
+
return "Annual";
|
|
101
|
+
}
|
|
102
|
+
return period;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function formatRateLimit(rate: unknown): string | null {
|
|
106
|
+
if (!rate || typeof rate !== "object") {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const { calls, renewalPeriodSeconds } = rate as {
|
|
111
|
+
calls?: number;
|
|
112
|
+
renewalPeriodSeconds?: number;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (calls == null || renewalPeriodSeconds == null) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const window =
|
|
120
|
+
renewalPeriodSeconds === 60 ? "minute" : `${renewalPeriodSeconds} seconds`;
|
|
121
|
+
|
|
122
|
+
return `${calls} request${calls === 1 ? "" : "s"} per ${window}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function summarizeLimits(limitsJson: string | null): string | null {
|
|
126
|
+
if (!limitsJson?.trim()) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const limits = JSON.parse(limitsJson) as Record<string, unknown>;
|
|
132
|
+
|
|
133
|
+
const rateSummary = formatRateLimit(limits.rateLimit);
|
|
134
|
+
if (rateSummary) {
|
|
135
|
+
return rateSummary;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const entries = Object.entries(limits).filter(
|
|
139
|
+
([, value]) => value != null && typeof value !== "object",
|
|
140
|
+
);
|
|
141
|
+
if (entries.length === 0) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return entries
|
|
146
|
+
.slice(0, 3)
|
|
147
|
+
.map(([key, value]) => `${key}: ${String(value)}`)
|
|
148
|
+
.join(" · ");
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function isActiveSubscription(status: string): boolean {
|
|
155
|
+
return status.toLowerCase() === "active";
|
|
156
|
+
}
|