Skip to content

Commit 463f5c8

Browse files
committed
Add reason code type to team capabilities response
1 parent e651d0a commit 463f5c8

File tree

5 files changed

+129
-54
lines changed

5 files changed

+129
-54
lines changed

.changeset/fuzzy-bars-wish.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@thirdweb-dev/service-utils": patch
3+
---
4+
5+
add reason code type into team capabilities response

apps/dashboard/src/@/storybook/stubs.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ export function teamStub(id: string, billingPlan: Team["billingPlan"]): Team {
7474
rpc: {
7575
enabled: true,
7676
rateLimit: 1000,
77+
websockets: {
78+
enabled: false,
79+
reasonCode: "enterprise_plan_required",
80+
maxConnections: 0,
81+
maxSubscriptions: 0,
82+
},
7783
},
7884
storage: {
7985
download: {

packages/service-utils/src/core/api.ts

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -55,71 +55,73 @@ export type ApiResponse = {
5555

5656
/**
5757
* Stores service-specific capabilities.
58-
* This type should match the schema from API server.
58+
* These types should match the schema from API server.
5959
*/
60+
export type ReasonCode =
61+
| "free_limit_exceeded"
62+
| "subscription_required"
63+
| "invoice_past_due"
64+
| "enterprise_plan_required"
65+
| "other";
66+
type EnabledWithReason<T> = T &
67+
({ enabled: true } | { enabled: false; reasonCode: ReasonCode });
6068
type TeamCapabilities = {
6169
platform: {
6270
auditLogs: boolean;
6371
ecosystemWallets: boolean;
6472
seats: boolean;
6573
};
66-
rpc: {
67-
enabled: boolean;
74+
rpc: EnabledWithReason<{
6875
rateLimit: number;
69-
};
70-
insight: {
71-
enabled: boolean;
76+
websockets: EnabledWithReason<{
77+
maxConnections: number;
78+
maxSubscriptions: number;
79+
}>;
80+
}>;
81+
insight: EnabledWithReason<{
7282
rateLimit: number;
7383
webhooks: boolean;
74-
};
75-
storage: {
76-
enabled: boolean;
84+
}>;
85+
storage: EnabledWithReason<{
7786
download: {
7887
rateLimit: number;
7988
};
8089
upload: {
8190
totalFileSizeBytesLimit: number;
8291
rateLimit: number;
8392
};
84-
};
85-
nebula: {
86-
enabled: boolean;
93+
}>;
94+
nebula: EnabledWithReason<{
8795
rateLimit: {
8896
perSecond: number;
8997
perMinute: number;
9098
};
91-
};
92-
bundler: {
93-
enabled: boolean;
99+
}>;
100+
bundler: EnabledWithReason<{
94101
mainnetEnabled: boolean;
95102
rateLimit: number;
96-
};
97-
embeddedWallets: {
98-
enabled: boolean;
103+
}>;
104+
embeddedWallets: EnabledWithReason<{
99105
customAuth: boolean;
100106
customBranding: boolean;
101107
sms: {
102108
domestic: boolean;
103109
international: boolean;
104110
};
105-
};
106-
engineCloud: {
107-
enabled: boolean;
111+
}>;
112+
engineCloud: EnabledWithReason<{
108113
mainnetEnabled: boolean;
109114
rateLimit: number;
110-
};
111-
pay: {
112-
enabled: boolean;
115+
}>;
116+
pay: EnabledWithReason<{
113117
rateLimit: number;
114-
};
115-
mcp: {
116-
enabled: boolean;
118+
}>;
119+
mcp: EnabledWithReason<{
117120
rateLimit: number;
118-
};
119-
gateway: {
120-
enabled: boolean;
121+
}>;
122+
gateway: EnabledWithReason<{
121123
rateLimit: number;
122-
};
124+
}>;
123125
};
124126

125127
type TeamPlan =

packages/service-utils/src/core/authorize/index.ts

Lines changed: 79 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
type CoreServiceConfig,
33
fetchTeamAndProject,
4+
type ReasonCode,
45
type TeamAndProjectResponse,
56
type TeamResponse,
67
} from "../api.js";
@@ -139,20 +140,55 @@ export async function authorize(
139140
};
140141
}
141142
// check if the service is maybe disabled for the team (usually due to a billing issue / exceeding the free plan limit)
142-
if (
143-
!isServiceEnabledForTeam(
144-
serviceConfig.serviceScope,
145-
teamAndProjectResponse.team.capabilities,
146-
)
147-
) {
148-
return {
149-
authorized: false,
150-
errorCode: "SERVICE_TEMPORARILY_DISABLED",
151-
errorMessage:
152-
"You currently do not have access to this service. Please check if your subscription includes this service and is active.",
153-
status: 403,
154-
};
143+
const disabledReason = getServiceDisabledReason(
144+
serviceConfig.serviceScope,
145+
teamAndProjectResponse.team.capabilities,
146+
);
147+
if (disabledReason) {
148+
switch (disabledReason) {
149+
case "enterprise_plan_required": {
150+
return {
151+
authorized: false,
152+
errorCode: "ENTERPRISE_PLAN_REQUIRED",
153+
errorMessage: `You currently do not have access to this feature. Please reach out to us to upgrade your plan to enable this feature: https://thirdweb.com/team/${teamAndProjectResponse.team.slug}/~/support`,
154+
status: 402,
155+
};
156+
}
157+
case "free_limit_exceeded": {
158+
return {
159+
authorized: false,
160+
errorCode: "FREE_LIMIT_EXCEEDED",
161+
errorMessage: `You have exceeded the free limit for this service. Find a plan that suits your needs to continue using this feature: https://thirdweb.com/team/${teamAndProjectResponse.team.slug}/~/billing?showPlans=true&highlight=growth`,
162+
status: 402,
163+
};
164+
}
165+
case "subscription_required": {
166+
return {
167+
authorized: false,
168+
errorCode: "SUBSCRIPTION_REQUIRED",
169+
errorMessage: `You need a subscription to use this feature. Find a plan that suits your needs to continue using this feature: https://thirdweb.com/team/${teamAndProjectResponse.team.slug}/~/billing?showPlans=true&highlight=growth`,
170+
status: 402,
171+
};
172+
}
173+
case "invoice_past_due": {
174+
return {
175+
authorized: false,
176+
errorCode: "INVOICE_PAST_DUE",
177+
errorMessage: `Please pay any outstanding invoices to continue using this feature: https://thirdweb.com/team/${teamAndProjectResponse.team.slug}/~/billing/invoices`,
178+
status: 402,
179+
};
180+
}
181+
default: {
182+
return {
183+
authorized: false,
184+
errorCode: "SERVICE_TEMPORARILY_DISABLED",
185+
errorMessage: `Access to this feature is temporarily restricted. Please reach out to us to resolve this issue: https://thirdweb.com/team/${teamAndProjectResponse.team.slug}/~/support`,
186+
status: 403,
187+
};
188+
}
189+
}
155190
}
191+
156192
// now we can validate the key itself
157193
const clientAuth = authorizeClient(authData, teamAndProjectResponse);
158194

@@ -186,25 +222,45 @@ export async function authorize(
186222
};
187223
}
188224

189-
function isServiceEnabledForTeam(
225+
function getServiceDisabledReason(
190226
scope: CoreServiceConfig["serviceScope"],
191227
teamCapabilities: TeamResponse["capabilities"],
192-
): boolean {
228+
): ReasonCode | null {
193229
switch (scope) {
194230
case "rpc":
195-
return teamCapabilities.rpc.enabled;
231+
return teamCapabilities.rpc.enabled
232+
? null
233+
: teamCapabilities.rpc.reasonCode;
196234
case "bundler":
197-
return teamCapabilities.bundler.enabled;
235+
return teamCapabilities.bundler.enabled
236+
? null
237+
: teamCapabilities.bundler.reasonCode;
198238
case "storage":
199-
return teamCapabilities.storage.enabled;
239+
return teamCapabilities.storage.enabled
240+
? null
241+
: teamCapabilities.storage.reasonCode;
200242
case "insight":
201-
return teamCapabilities.insight.enabled;
243+
return teamCapabilities.insight.enabled
244+
? null
245+
: teamCapabilities.insight.reasonCode;
202246
case "nebula":
203-
return teamCapabilities.nebula.enabled;
247+
return teamCapabilities.nebula.enabled
248+
? null
249+
: teamCapabilities.nebula.reasonCode;
204250
case "embeddedWallets":
205-
return teamCapabilities.embeddedWallets.enabled;
251+
return teamCapabilities.embeddedWallets.enabled
252+
? null
253+
: teamCapabilities.embeddedWallets.reasonCode;
254+
case "engineCloud":
255+
return teamCapabilities.engineCloud.enabled
256+
? null
257+
: teamCapabilities.engineCloud.reasonCode;
258+
case "pay":
259+
return teamCapabilities.pay.enabled
260+
? null
261+
: teamCapabilities.pay.reasonCode;
206262
default:
207-
// always return true for any legacy / un-named services
208-
return true;
263+
// always return null for any legacy / un-named services
264+
return null;
209265
}
210266
}

packages/service-utils/src/mocks.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ export const validTeamResponse: TeamResponse = {
9292
rpc: {
9393
enabled: true,
9494
rateLimit: 1000,
95+
websockets: {
96+
enabled: false,
97+
reasonCode: "enterprise_plan_required",
98+
maxConnections: 0,
99+
maxSubscriptions: 0,
100+
},
95101
},
96102
storage: {
97103
download: {

0 commit comments

Comments
 (0)