Skip to content

Commit a6aa235

Browse files
authored
refactor(console): add get tenant add-on-skus endpoint (#6886)
* refactor(console): add get tenant add-on-skus endpoint dynamicly display token usage tooltips content based on the token add-on SKU details * fix: fix email connector ts error fix email connector ts error
1 parent 8fc5e25 commit a6aa235

File tree

27 files changed

+118
-62
lines changed

27 files changed

+118
-62
lines changed

packages/connectors/connector-logto-email/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"access": "public"
5353
},
5454
"devDependencies": {
55-
"@logto/cloud": "0.2.5-5e334eb",
55+
"@logto/cloud": "0.2.5-aac51e9",
5656
"@silverhand/eslint-config": "6.0.1",
5757
"@silverhand/ts-config": "6.0.0",
5858
"@types/node": "^20.11.20",

packages/connectors/connector-logto-email/src/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,6 @@ const sendMessage =
4040
body: {
4141
data: {
4242
to,
43-
// TODO @wangsijie: fix this circular dependency, the connector-kit type change should be released first
44-
// @ts-expect-error circular dependency
4543
type,
4644
payload: {
4745
...payload,

packages/console/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"devDependencies": {
2828
"@fontsource/roboto-mono": "^5.0.0",
2929
"@jest/types": "^29.5.0",
30-
"@logto/cloud": "0.2.5-5e334eb",
30+
"@logto/cloud": "0.2.5-aac51e9",
3131
"@logto/connector-kit": "workspace:^4.1.0",
3232
"@logto/core-kit": "workspace:^2.5.0",
3333
"@logto/language-kit": "workspace:^1.1.0",

packages/console/src/cloud/types/router.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ export type LogtoSkuResponse = GetArrayElementType<GuardedResponse<GetRoutes['/a
1111

1212
export type Subscription = GuardedResponse<GetRoutes['/api/tenants/:tenantId/subscription']>;
1313

14+
export type TenantUsageAddOnSkus = GuardedResponse<
15+
GetRoutes['/api/tenants/:tenantId/subscription/add-on-skus']
16+
>;
17+
1418
/* ===== Use `New` in the naming to avoid confusion with legacy types ===== */
1519
export type NewSubscriptionUsageResponse = GuardedResponse<
1620
GetRoutes['/api/tenants/:tenantId/subscription-usage']

packages/console/src/components/PlanUsage/PlanUsageCard/index.tsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useContext } from 'react';
55
import { Trans, useTranslation } from 'react-i18next';
66

77
import Tip from '@/assets/icons/tip.svg?react';
8+
import { type LogtoSkuResponse } from '@/cloud/types/router';
89
import { addOnPricingExplanationLink } from '@/consts/external-links';
910
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
1011
import DynamicT from '@/ds-components/DynamicT';
@@ -60,6 +61,30 @@ const formatNumberTypedUsageDescription = ({
6061
return `${formatNumber(usage)} / ${unlimitedString}`;
6162
};
6263

64+
/**
65+
* The price unit returned from DB is cent, so we need to divide it by 100 to get the dollar price.
66+
*/
67+
const formatDecimalPrice = (price: number): string => {
68+
return (price / 100).toFixed(2);
69+
};
70+
71+
// Manually format the quota display for add-on usages
72+
const formatAddOnQuota = (quota?: LogtoSkuResponse['quota']) => {
73+
if (!quota) {
74+
return;
75+
}
76+
77+
return {
78+
...quota,
79+
...conditional(quota.tokenLimit && { tokenLimit: formatQuotaNumber(quota.tokenLimit) }),
80+
};
81+
};
82+
83+
/**
84+
* @param unitPrice Hardcoded add-on unit price. Only used for the tooltip.
85+
* @param usageAddOnSku The add-on SKU object. Only used for the tooltip.
86+
* If provided, use the unit price and count from the SKU object first. Otherwise, fallback to the hardcoded unit price.
87+
*/
6388
export type Props = {
6489
readonly usage: number | boolean;
6590
readonly quota?: Nullable<number> | boolean;
@@ -70,6 +95,7 @@ export type Props = {
7095
readonly unitPrice: number;
7196
readonly className?: string;
7297
readonly isQuotaNoticeHidden?: boolean;
98+
readonly usageAddOnSku?: LogtoSkuResponse;
7399
};
74100

75101
function PlanUsageCard({
@@ -82,6 +108,7 @@ function PlanUsageCard({
82108
tooltipKey,
83109
className,
84110
isQuotaNoticeHidden,
111+
usageAddOnSku,
85112
}: Props) {
86113
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
87114
const {
@@ -109,12 +136,15 @@ function PlanUsageCard({
109136
}}
110137
>
111138
{t(tooltipKey, {
112-
price: unitPrice,
139+
price: usageAddOnSku?.unitPrice
140+
? formatDecimalPrice(usageAddOnSku.unitPrice)
141+
: unitPrice,
113142
...conditional(
114143
typeof basicQuota === 'number' && {
115144
basicQuota: formatQuotaNumber(basicQuota),
116145
}
117146
),
147+
...conditional(usageAddOnSku && formatAddOnQuota(usageAddOnSku.quota)),
118148
})}
119149
</Trans>
120150
}

packages/console/src/components/PlanUsage/index.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
type NewSubscriptionPeriodicUsage,
99
type NewSubscriptionCountBasedUsage,
1010
type NewSubscriptionQuota,
11+
type TenantUsageAddOnSkus,
1112
} from '@/cloud/types/router';
1213
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
1314
import DynamicT from '@/ds-components/DynamicT';
@@ -26,6 +27,7 @@ import {
2627

2728
type Props = {
2829
readonly periodicUsage: NewSubscriptionPeriodicUsage | undefined;
30+
readonly usageAddOnSkus?: TenantUsageAddOnSkus;
2931
};
3032

3133
const getUsageByKey = (
@@ -57,7 +59,7 @@ const getUsageByKey = (
5759
return countBasedUsage[key];
5860
};
5961

60-
function PlanUsage({ periodicUsage }: Props) {
62+
function PlanUsage({ periodicUsage, usageAddOnSkus }: Props) {
6163
const {
6264
currentSubscriptionQuota,
6365
currentSubscriptionBasicQuota,
@@ -87,6 +89,8 @@ function PlanUsage({ periodicUsage }: Props) {
8789
usageKey: 'subscription.usage.usage_description_with_limited_quota',
8890
titleKey: `subscription.usage.${titleKeyMap[key]}`,
8991
unitPrice: usageKeyPriceMap[key],
92+
// Only support tokenLimit for now
93+
usageAddOnSku: cond(key === 'tokenLimit' && usageAddOnSkus?.[key]),
9094
...cond(
9195
// We only show the usage card for MAU and token for Free plan
9296
(key === 'tokenLimit' || key === 'mauLimit' || isPaidTenant) && {

packages/console/src/hooks/use-subscribe.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ const useSubscribe = () => {
6464
const { redirectUri, sessionId } = await cloudApi.post('/api/checkout-session', {
6565
body: {
6666
skuId,
67-
planId,
6867
successCallbackUrl,
6968
tenantId,
7069
tenantName: tenantData?.name,

packages/console/src/pages/TenantSettings/Subscription/CurrentPlan/index.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useContext, useMemo } from 'react';
22

3-
import { type NewSubscriptionPeriodicUsage } from '@/cloud/types/router';
3+
import { type TenantUsageAddOnSkus, type NewSubscriptionPeriodicUsage } from '@/cloud/types/router';
44
import BillInfo from '@/components/BillInfo';
55
import FormCard from '@/components/FormCard';
66
import PlanDescription from '@/components/PlanDescription';
@@ -17,9 +17,10 @@ import styles from './index.module.scss';
1717

1818
type Props = {
1919
readonly periodicUsage?: NewSubscriptionPeriodicUsage;
20+
readonly usageAddOnSkus?: TenantUsageAddOnSkus;
2021
};
2122

22-
function CurrentPlan({ periodicUsage }: Props) {
23+
function CurrentPlan({ periodicUsage, usageAddOnSkus }: Props) {
2324
const {
2425
currentSku: { unitPrice },
2526
currentSubscription: { upcomingInvoice, isEnterprisePlan, planId },
@@ -49,7 +50,7 @@ function CurrentPlan({ periodicUsage }: Props) {
4950
</div>
5051
</div>
5152
<FormField title="subscription.plan_usage">
52-
<PlanUsage periodicUsage={periodicUsage} />
53+
<PlanUsage periodicUsage={periodicUsage} usageAddOnSkus={usageAddOnSkus} />
5354
</FormField>
5455
<FormField title="subscription.next_bill">
5556
<BillInfo cost={upcomingCost} isManagePaymentVisible={Boolean(upcomingCost)} />

packages/console/src/pages/TenantSettings/Subscription/index.tsx

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import { type ResponseError } from '@withtyped/client';
12
import { useContext, useEffect } from 'react';
23
import useSWR from 'swr';
34

45
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
6+
import { type TenantUsageAddOnSkus, type NewSubscriptionPeriodicUsage } from '@/cloud/types/router';
57
import PageMeta from '@/components/PageMeta';
6-
import { isCloud } from '@/consts/env';
8+
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
79
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
810
import { TenantsContext } from '@/contexts/TenantsProvider';
911
import { pickupFeaturedLogtoSkus } from '@/utils/subscription';
@@ -23,14 +25,27 @@ function Subscription() {
2325

2426
const reservedSkus = pickupFeaturedLogtoSkus(logtoSkus);
2527

26-
const { data: periodicUsage, isLoading } = useSWR(
27-
isCloud && `/api/tenants/${currentTenantId}/subscription/periodic-usage`,
28-
async () =>
29-
cloudApi.get(`/api/tenants/:tenantId/subscription/periodic-usage`, {
30-
params: { tenantId: currentTenantId },
31-
})
28+
const { data: periodicUsage, error: periodicUsageError } = useSWR<
29+
NewSubscriptionPeriodicUsage,
30+
ResponseError
31+
>(isCloud && `/api/tenants/${currentTenantId}/subscription/periodic-usage`, async () =>
32+
cloudApi.get(`/api/tenants/:tenantId/subscription/periodic-usage`, {
33+
params: { tenantId: currentTenantId },
34+
})
3235
);
3336

37+
const { data: usageAddOnSkus, error: usageAddOnSkusError } = useSWR<
38+
TenantUsageAddOnSkus,
39+
ResponseError
40+
>(isCloud && isDevFeaturesEnabled && `/api/tenants/${currentTenantId}/add-on-skus`, async () =>
41+
cloudApi.get(`/api/tenants/:tenantId/subscription/add-on-skus`, {
42+
params: { tenantId: currentTenantId },
43+
})
44+
);
45+
46+
const isLoading =
47+
(!periodicUsage && !periodicUsageError) || (!usageAddOnSkus && !usageAddOnSkusError);
48+
3449
useEffect(() => {
3550
if (isCloud) {
3651
onCurrentSubscriptionUpdated();
@@ -55,7 +70,7 @@ function Subscription() {
5570
return (
5671
<div className={styles.container}>
5772
<PageMeta titleKey={['tenants.tabs.subscription', 'tenants.title']} />
58-
<CurrentPlan periodicUsage={periodicUsage} />
73+
<CurrentPlan periodicUsage={periodicUsage} usageAddOnSkus={usageAddOnSkus} />
5974
<PlanComparisonTable />
6075
<SwitchPlanActionBar
6176
currentSkuId={currentSku.id}

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
"zod": "^3.23.8"
100100
},
101101
"devDependencies": {
102-
"@logto/cloud": "0.2.5-5e334eb",
102+
"@logto/cloud": "0.2.5-aac51e9",
103103
"@silverhand/eslint-config": "6.0.1",
104104
"@silverhand/ts-config": "6.0.0",
105105
"@types/adm-zip": "^0.5.5",

0 commit comments

Comments
 (0)