@wopr-network/platform-ui-core 1.23.0 → 1.24.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/package.json +1 -1
- package/src/__tests__/create-instance.test.tsx +12 -0
- package/src/__tests__/org-billing-api.test.tsx +9 -15
- package/src/__tests__/org-billing-null-guards.test.ts +13 -15
- package/src/app/(dashboard)/layout.tsx +2 -2
- package/src/lib/__tests__/org-billing-api.test.ts +46 -115
- package/src/lib/brand-config.ts +4 -0
package/package.json
CHANGED
|
@@ -3,6 +3,18 @@ import userEvent from "@testing-library/user-event";
|
|
|
3
3
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
4
|
import { CreateInstanceClient } from "../app/instances/new/create-instance-client";
|
|
5
5
|
|
|
6
|
+
vi.mock("next/navigation", () => ({
|
|
7
|
+
useRouter: vi.fn().mockReturnValue({
|
|
8
|
+
push: vi.fn(),
|
|
9
|
+
replace: vi.fn(),
|
|
10
|
+
prefetch: vi.fn(),
|
|
11
|
+
back: vi.fn(),
|
|
12
|
+
refresh: vi.fn(),
|
|
13
|
+
}),
|
|
14
|
+
usePathname: vi.fn().mockReturnValue("/instances/new"),
|
|
15
|
+
useSearchParams: vi.fn().mockReturnValue(new URLSearchParams()),
|
|
16
|
+
}));
|
|
17
|
+
|
|
6
18
|
vi.mock("@/lib/marketplace-data", () => ({
|
|
7
19
|
listMarketplacePlugins: vi.fn(),
|
|
8
20
|
}));
|
|
@@ -2,25 +2,19 @@ import { describe, expect, it, vi } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
vi.mock("@/lib/trpc", () => ({
|
|
4
4
|
trpcVanilla: {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
query: vi.fn().mockResolvedValue({
|
|
8
|
-
orgId: "org-1",
|
|
9
|
-
balanceCents: 5000,
|
|
10
|
-
dailyBurnCents: 100,
|
|
11
|
-
runwayDays: 50,
|
|
12
|
-
}),
|
|
13
|
-
},
|
|
14
|
-
orgMemberUsage: {
|
|
5
|
+
billing: {
|
|
6
|
+
creditsBalance: {
|
|
15
7
|
query: vi.fn().mockResolvedValue({
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
balance_credits: 5000,
|
|
9
|
+
daily_burn_credits: 100,
|
|
10
|
+
runway_days: 50,
|
|
19
11
|
}),
|
|
20
12
|
},
|
|
21
|
-
|
|
13
|
+
billingInfo: {
|
|
22
14
|
query: vi.fn().mockResolvedValue({ paymentMethods: [], invoices: [] }),
|
|
23
15
|
},
|
|
16
|
+
},
|
|
17
|
+
org: {
|
|
24
18
|
orgTopupCheckout: {
|
|
25
19
|
mutate: vi.fn().mockResolvedValue({
|
|
26
20
|
url: "https://checkout.stripe.com/test",
|
|
@@ -42,7 +36,7 @@ import {
|
|
|
42
36
|
} from "@/lib/org-billing-api";
|
|
43
37
|
|
|
44
38
|
describe("org-billing-api", () => {
|
|
45
|
-
it("getOrgCreditBalance converts
|
|
39
|
+
it("getOrgCreditBalance converts credits to dollars", async () => {
|
|
46
40
|
const result = await getOrgCreditBalance("org-1");
|
|
47
41
|
expect(result.balance).toBe(50);
|
|
48
42
|
expect(result.dailyBurn).toBe(1);
|
|
@@ -9,10 +9,11 @@ interface MockMutate {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
interface MockTrpcVanilla {
|
|
12
|
+
billing: {
|
|
13
|
+
creditsBalance: MockQuery;
|
|
14
|
+
billingInfo: MockQuery;
|
|
15
|
+
};
|
|
12
16
|
org: {
|
|
13
|
-
orgBillingBalance: MockQuery;
|
|
14
|
-
orgMemberUsage: MockQuery;
|
|
15
|
-
orgBillingInfo: MockQuery;
|
|
16
17
|
orgTopupCheckout: MockMutate;
|
|
17
18
|
orgRemovePaymentMethod: MockMutate;
|
|
18
19
|
};
|
|
@@ -20,10 +21,11 @@ interface MockTrpcVanilla {
|
|
|
20
21
|
|
|
21
22
|
vi.mock("@/lib/trpc", () => ({
|
|
22
23
|
trpcVanilla: {
|
|
24
|
+
billing: {
|
|
25
|
+
creditsBalance: { query: vi.fn() },
|
|
26
|
+
billingInfo: { query: vi.fn() },
|
|
27
|
+
},
|
|
23
28
|
org: {
|
|
24
|
-
orgBillingBalance: { query: vi.fn() },
|
|
25
|
-
orgMemberUsage: { query: vi.fn() },
|
|
26
|
-
orgBillingInfo: { query: vi.fn() },
|
|
27
29
|
orgTopupCheckout: { mutate: vi.fn() },
|
|
28
30
|
orgRemovePaymentMethod: { mutate: vi.fn() },
|
|
29
31
|
},
|
|
@@ -33,8 +35,8 @@ vi.mock("@/lib/trpc", () => ({
|
|
|
33
35
|
describe("org-billing-api null guards", () => {
|
|
34
36
|
it("getOrgCreditBalance handles empty response", async () => {
|
|
35
37
|
const { trpcVanilla } = await import("@/lib/trpc");
|
|
36
|
-
const {
|
|
37
|
-
|
|
38
|
+
const { billing } = trpcVanilla as unknown as MockTrpcVanilla;
|
|
39
|
+
billing.creditsBalance.query.mockResolvedValue({});
|
|
38
40
|
|
|
39
41
|
const { getOrgCreditBalance } = await import("@/lib/org-billing-api");
|
|
40
42
|
const result = await getOrgCreditBalance("org-1");
|
|
@@ -43,11 +45,7 @@ describe("org-billing-api null guards", () => {
|
|
|
43
45
|
expect(result.runway).toBeNull();
|
|
44
46
|
});
|
|
45
47
|
|
|
46
|
-
it("getOrgMemberUsage
|
|
47
|
-
const { trpcVanilla } = await import("@/lib/trpc");
|
|
48
|
-
const { org } = trpcVanilla as unknown as MockTrpcVanilla;
|
|
49
|
-
org.orgMemberUsage.query.mockResolvedValue({ orgId: "o", periodStart: "2026-01-01" });
|
|
50
|
-
|
|
48
|
+
it("getOrgMemberUsage returns stub with empty members", async () => {
|
|
51
49
|
const { getOrgMemberUsage } = await import("@/lib/org-billing-api");
|
|
52
50
|
const result = await getOrgMemberUsage("org-1");
|
|
53
51
|
expect(result.members).toEqual([]);
|
|
@@ -55,8 +53,8 @@ describe("org-billing-api null guards", () => {
|
|
|
55
53
|
|
|
56
54
|
it("getOrgBillingInfo handles empty response", async () => {
|
|
57
55
|
const { trpcVanilla } = await import("@/lib/trpc");
|
|
58
|
-
const {
|
|
59
|
-
|
|
56
|
+
const { billing } = trpcVanilla as unknown as MockTrpcVanilla;
|
|
57
|
+
billing.billingInfo.query.mockResolvedValue({});
|
|
60
58
|
|
|
61
59
|
const { getOrgBillingInfo } = await import("@/lib/org-billing-api");
|
|
62
60
|
const result = await getOrgBillingInfo("org-1");
|
|
@@ -24,7 +24,7 @@ import { Button } from "@/components/ui/button";
|
|
|
24
24
|
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
|
25
25
|
import { usePageContext } from "@/hooks/use-page-context";
|
|
26
26
|
import { useWebMCP } from "@/hooks/use-webmcp";
|
|
27
|
-
import { productName } from "@/lib/brand-config";
|
|
27
|
+
import { getBrandConfig, productName } from "@/lib/brand-config";
|
|
28
28
|
import { ChatProvider } from "@/lib/chat/chat-context";
|
|
29
29
|
|
|
30
30
|
export default function DashboardLayout({
|
|
@@ -107,7 +107,7 @@ export default function DashboardLayout({
|
|
|
107
107
|
</motion.main>
|
|
108
108
|
</AnimatePresence>
|
|
109
109
|
</div>
|
|
110
|
-
{!pathname.startsWith("/chat") && <ChatWidget />}
|
|
110
|
+
{getBrandConfig().chatEnabled && !pathname.startsWith("/chat") && <ChatWidget />}
|
|
111
111
|
</ChatProvider>
|
|
112
112
|
);
|
|
113
113
|
}
|
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
3
|
const {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
mockOrgBillingInfoQuery,
|
|
4
|
+
mockCreditsBalanceQuery,
|
|
5
|
+
mockBillingInfoQuery,
|
|
7
6
|
mockOrgTopupCheckoutMutate,
|
|
8
7
|
mockOrgSetDefaultPaymentMethodMutate,
|
|
9
8
|
} = vi.hoisted(() => ({
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
mockOrgBillingInfoQuery: vi.fn(),
|
|
9
|
+
mockCreditsBalanceQuery: vi.fn(),
|
|
10
|
+
mockBillingInfoQuery: vi.fn(),
|
|
13
11
|
mockOrgTopupCheckoutMutate: vi.fn(),
|
|
14
12
|
mockOrgSetDefaultPaymentMethodMutate: vi.fn(),
|
|
15
13
|
}));
|
|
16
14
|
|
|
17
15
|
vi.mock("@/lib/trpc", () => ({
|
|
18
16
|
trpcVanilla: {
|
|
17
|
+
billing: {
|
|
18
|
+
creditsBalance: { query: mockCreditsBalanceQuery },
|
|
19
|
+
billingInfo: { query: mockBillingInfoQuery },
|
|
20
|
+
},
|
|
19
21
|
org: {
|
|
20
|
-
orgBillingBalance: { query: mockOrgBillingBalanceQuery },
|
|
21
|
-
orgMemberUsage: { query: mockOrgMemberUsageQuery },
|
|
22
|
-
orgBillingInfo: { query: mockOrgBillingInfoQuery },
|
|
23
22
|
orgTopupCheckout: { mutate: mockOrgTopupCheckoutMutate },
|
|
24
23
|
orgSetDefaultPaymentMethod: { mutate: mockOrgSetDefaultPaymentMethodMutate },
|
|
25
24
|
},
|
|
@@ -38,11 +37,11 @@ import {
|
|
|
38
37
|
describe("getOrgCreditBalance", () => {
|
|
39
38
|
afterEach(() => vi.clearAllMocks());
|
|
40
39
|
|
|
41
|
-
it("converts
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
it("converts credits to dollars and returns balance", async () => {
|
|
41
|
+
mockCreditsBalanceQuery.mockResolvedValue({
|
|
42
|
+
balance_credits: 5000,
|
|
43
|
+
daily_burn_credits: 200,
|
|
44
|
+
runway_days: 25,
|
|
46
45
|
});
|
|
47
46
|
|
|
48
47
|
const result = await getOrgCreditBalance("org-1");
|
|
@@ -51,14 +50,29 @@ describe("getOrgCreditBalance", () => {
|
|
|
51
50
|
dailyBurn: 2,
|
|
52
51
|
runway: 25,
|
|
53
52
|
});
|
|
54
|
-
expect(
|
|
53
|
+
expect(mockCreditsBalanceQuery).toHaveBeenCalledWith({});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("falls back to legacy cents fields", async () => {
|
|
57
|
+
mockCreditsBalanceQuery.mockResolvedValue({
|
|
58
|
+
balance_cents: 3000,
|
|
59
|
+
daily_burn_cents: 100,
|
|
60
|
+
runway_days: 30,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const result = await getOrgCreditBalance("org-1");
|
|
64
|
+
expect(result).toEqual({
|
|
65
|
+
balance: 30,
|
|
66
|
+
dailyBurn: 1,
|
|
67
|
+
runway: 30,
|
|
68
|
+
});
|
|
55
69
|
});
|
|
56
70
|
|
|
57
71
|
it("defaults to zero balance when response fields are null", async () => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
mockCreditsBalanceQuery.mockResolvedValue({
|
|
73
|
+
balance_credits: null,
|
|
74
|
+
daily_burn_credits: null,
|
|
75
|
+
runway_days: null,
|
|
62
76
|
});
|
|
63
77
|
|
|
64
78
|
const result = await getOrgCreditBalance("org-1");
|
|
@@ -70,7 +84,7 @@ describe("getOrgCreditBalance", () => {
|
|
|
70
84
|
});
|
|
71
85
|
|
|
72
86
|
it("defaults to zero balance when response fields are undefined", async () => {
|
|
73
|
-
|
|
87
|
+
mockCreditsBalanceQuery.mockResolvedValue({});
|
|
74
88
|
|
|
75
89
|
const result = await getOrgCreditBalance("org-1");
|
|
76
90
|
expect(result).toEqual({
|
|
@@ -81,101 +95,17 @@ describe("getOrgCreditBalance", () => {
|
|
|
81
95
|
});
|
|
82
96
|
|
|
83
97
|
it("propagates tRPC errors", async () => {
|
|
84
|
-
|
|
98
|
+
mockCreditsBalanceQuery.mockRejectedValue(new Error("Forbidden"));
|
|
85
99
|
await expect(getOrgCreditBalance("org-1")).rejects.toThrow("Forbidden");
|
|
86
100
|
});
|
|
87
101
|
});
|
|
88
102
|
|
|
89
103
|
describe("getOrgMemberUsage", () => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
it("transforms member usage with cents-to-dollars conversion", async () => {
|
|
93
|
-
mockOrgMemberUsageQuery.mockResolvedValue({
|
|
94
|
-
orgId: "org-1",
|
|
95
|
-
periodStart: "2026-03-01",
|
|
96
|
-
members: [
|
|
97
|
-
{
|
|
98
|
-
memberId: "m-1",
|
|
99
|
-
name: "Alice",
|
|
100
|
-
email: "alice@test.com",
|
|
101
|
-
creditsConsumedCents: 1500,
|
|
102
|
-
lastActiveAt: "2026-03-02T10:00:00Z",
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
memberId: "m-2",
|
|
106
|
-
name: "Bob",
|
|
107
|
-
email: "bob@test.com",
|
|
108
|
-
creditsConsumedCents: 300,
|
|
109
|
-
lastActiveAt: null,
|
|
110
|
-
},
|
|
111
|
-
],
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
const result = await getOrgMemberUsage("org-1");
|
|
115
|
-
expect(result).toEqual({
|
|
116
|
-
orgId: "org-1",
|
|
117
|
-
periodStart: "2026-03-01",
|
|
118
|
-
members: [
|
|
119
|
-
{
|
|
120
|
-
memberId: "m-1",
|
|
121
|
-
name: "Alice",
|
|
122
|
-
email: "alice@test.com",
|
|
123
|
-
creditsConsumed: 15,
|
|
124
|
-
lastActiveAt: "2026-03-02T10:00:00Z",
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
memberId: "m-2",
|
|
128
|
-
name: "Bob",
|
|
129
|
-
email: "bob@test.com",
|
|
130
|
-
creditsConsumed: 3,
|
|
131
|
-
lastActiveAt: null,
|
|
132
|
-
},
|
|
133
|
-
],
|
|
134
|
-
});
|
|
135
|
-
expect(mockOrgMemberUsageQuery).toHaveBeenCalledWith({ orgId: "org-1" });
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("defaults member fields when properties are missing", async () => {
|
|
139
|
-
mockOrgMemberUsageQuery.mockResolvedValue({
|
|
140
|
-
orgId: "org-1",
|
|
141
|
-
periodStart: "2026-03-01",
|
|
142
|
-
members: [{}],
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
const result = await getOrgMemberUsage("org-1");
|
|
146
|
-
expect(result.members[0]).toEqual({
|
|
147
|
-
memberId: "",
|
|
148
|
-
name: "",
|
|
149
|
-
email: "",
|
|
150
|
-
creditsConsumed: 0,
|
|
151
|
-
lastActiveAt: null,
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("handles null members array", async () => {
|
|
156
|
-
mockOrgMemberUsageQuery.mockResolvedValue({
|
|
157
|
-
orgId: "org-1",
|
|
158
|
-
periodStart: "2026-03-01",
|
|
159
|
-
members: null,
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
const result = await getOrgMemberUsage("org-1");
|
|
163
|
-
expect(result.members).toEqual([]);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it("falls back orgId and periodStart when missing from response", async () => {
|
|
167
|
-
mockOrgMemberUsageQuery.mockResolvedValue({
|
|
168
|
-
members: [],
|
|
169
|
-
});
|
|
170
|
-
|
|
104
|
+
it("returns stub data with empty members array", async () => {
|
|
171
105
|
const result = await getOrgMemberUsage("org-1");
|
|
172
106
|
expect(result.orgId).toBe("org-1");
|
|
173
|
-
expect(result.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
it("propagates tRPC errors", async () => {
|
|
177
|
-
mockOrgMemberUsageQuery.mockRejectedValue(new Error("Server error"));
|
|
178
|
-
await expect(getOrgMemberUsage("org-1")).rejects.toThrow("Server error");
|
|
107
|
+
expect(result.members).toEqual([]);
|
|
108
|
+
expect(result.periodStart).toBeTruthy();
|
|
179
109
|
});
|
|
180
110
|
});
|
|
181
111
|
|
|
@@ -195,7 +125,7 @@ describe("getOrgBillingInfo", () => {
|
|
|
195
125
|
hostedLineItems: undefined,
|
|
196
126
|
},
|
|
197
127
|
];
|
|
198
|
-
|
|
128
|
+
mockBillingInfoQuery.mockResolvedValue({ paymentMethods, invoices });
|
|
199
129
|
|
|
200
130
|
const result = await getOrgBillingInfo("org-1");
|
|
201
131
|
expect(result).toEqual({
|
|
@@ -212,11 +142,11 @@ describe("getOrgBillingInfo", () => {
|
|
|
212
142
|
},
|
|
213
143
|
],
|
|
214
144
|
});
|
|
215
|
-
expect(
|
|
145
|
+
expect(mockBillingInfoQuery).toHaveBeenCalledWith({});
|
|
216
146
|
});
|
|
217
147
|
|
|
218
148
|
it("defaults to empty arrays when fields are null", async () => {
|
|
219
|
-
|
|
149
|
+
mockBillingInfoQuery.mockResolvedValue({
|
|
220
150
|
paymentMethods: null,
|
|
221
151
|
invoices: null,
|
|
222
152
|
});
|
|
@@ -226,15 +156,16 @@ describe("getOrgBillingInfo", () => {
|
|
|
226
156
|
});
|
|
227
157
|
|
|
228
158
|
it("defaults to empty arrays when fields are undefined", async () => {
|
|
229
|
-
|
|
159
|
+
mockBillingInfoQuery.mockResolvedValue({});
|
|
230
160
|
|
|
231
161
|
const result = await getOrgBillingInfo("org-1");
|
|
232
162
|
expect(result).toEqual({ paymentMethods: [], invoices: [] });
|
|
233
163
|
});
|
|
234
164
|
|
|
235
|
-
it("
|
|
236
|
-
|
|
237
|
-
await
|
|
165
|
+
it("returns defaults on tRPC error instead of throwing", async () => {
|
|
166
|
+
mockBillingInfoQuery.mockRejectedValue(new Error("Not found"));
|
|
167
|
+
const result = await getOrgBillingInfo("org-1");
|
|
168
|
+
expect(result).toEqual({ paymentMethods: [], invoices: [] });
|
|
238
169
|
});
|
|
239
170
|
});
|
|
240
171
|
|
package/src/lib/brand-config.ts
CHANGED
|
@@ -90,6 +90,9 @@ export interface BrandConfig {
|
|
|
90
90
|
|
|
91
91
|
/** Sidebar navigation items. Each has a label and href. */
|
|
92
92
|
navItems: Array<{ label: string; href: string }>;
|
|
93
|
+
|
|
94
|
+
/** Whether the embedded chat widget is enabled (default true). */
|
|
95
|
+
chatEnabled: boolean;
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
/**
|
|
@@ -154,6 +157,7 @@ function envDefaults(): BrandConfig {
|
|
|
154
157
|
companyLegalName: process.env.NEXT_PUBLIC_BRAND_COMPANY_LEGAL || "Platform Inc.",
|
|
155
158
|
price: process.env.NEXT_PUBLIC_BRAND_PRICE || "",
|
|
156
159
|
homePath: process.env.NEXT_PUBLIC_BRAND_HOME_PATH || "/marketplace",
|
|
160
|
+
chatEnabled: process.env.NEXT_PUBLIC_BRAND_CHAT_ENABLED !== "false",
|
|
157
161
|
navItems: parseNavItems(process.env.NEXT_PUBLIC_BRAND_NAV_ITEMS) ?? [
|
|
158
162
|
{ label: "Dashboard", href: "/dashboard" },
|
|
159
163
|
{ label: "Chat", href: "/chat" },
|