Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"@fontsource/roboto-mono": "^5.0.0",
"@inkeep/cxkit-react": "^0.5.66",
"@jest/types": "^29.5.0",
"@logto/cloud": "0.2.5-31c9868",
"@logto/cloud": "0.2.5-66a5752",
"@logto/connector-kit": "workspace:^",
"@logto/core-kit": "workspace:^",
"@logto/language-kit": "workspace:^",
Expand Down
22 changes: 16 additions & 6 deletions packages/console/src/cloud/types/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ type GetTenantAuthRoutes = RouterRoutes<typeof tenantAuthRouter>['get'];

export type GetArrayElementType<T> = T extends Array<infer U> ? U : never;

export type LogtoSkuResponse = GetArrayElementType<GuardedResponse<GetRoutes['/api/skus']>>;
type CloudLogtoSkuResponse = GetArrayElementType<GuardedResponse<GetRoutes['/api/skus']>>;

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

Expand All @@ -21,21 +21,31 @@ export type SubscriptionUsageResponse = GuardedResponse<
GetRoutes['/api/tenants/:tenantId/subscription-usage']
>;

type InlineHookSubscriptionQuota = {
inlineHooksEnabled: boolean;
};

export type SubscriptionQuota = Omit<
SubscriptionUsageResponse['quota'],
// Since we are deprecation the `organizationsEnabled` key soon (use `organizationsLimit` instead), we exclude it from the quota keys for now to avoid confusion.
// Since we are deprecating the `organizationsEnabled` key soon (use `organizationsLimit` instead), we exclude it from the quota keys for now to avoid confusion.
'organizationsEnabled'
>;
> &
InlineHookSubscriptionQuota;

export type LogtoSkuResponse = Omit<CloudLogtoSkuResponse, 'quota'> & {
quota: CloudLogtoSkuResponse['quota'] & Partial<InlineHookSubscriptionQuota>;
};

export type SubscriptionCountBasedUsage = Omit<
SubscriptionUsageResponse['usage'],
// Since we are deprecation the `organizationsEnabled` key soon (use `organizationsLimit` instead), we exclude it from the usage keys for now to avoid confusion.
// Since we are deprecating the `organizationsEnabled` key soon (use `organizationsLimit` instead), we exclude it from the usage keys for now to avoid confusion.
'organizationsEnabled'
>;
> &
InlineHookSubscriptionQuota;
export type SubscriptionResourceScopeUsage = SubscriptionUsageResponse['resources'];
export type SubscriptionRoleScopeUsage = Omit<
SubscriptionUsageResponse['roles'],
// Since we are deprecation the `organizationsEnabled` key soon (use `organizationsLimit` instead), we exclude it from the quota keys for now to avoid confusion.
// Since we are deprecating the `organizationsEnabled` key soon (use `organizationsLimit` instead), we exclude it from the quota keys for now to avoid confusion.
'organizationsEnabled'
>;

Expand Down
2 changes: 2 additions & 0 deletions packages/console/src/consts/plan-quotas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ export const comingSoonSkuQuotaKeys: Array<keyof LogtoSkuQuota> = [];
*/
export const hiddenQuotaDiffUsageKeys: Array<keyof LogtoSkuQuota> = [
'tokenLimit',
// Keep inline hooks hidden from the plan quota UI until it is ready to open after further testing.
'inlineHooksEnabled',
'scopesPerResourceLimit',
'userRolesLimit',
'machineToMachineRolesLimit',
Expand Down
13 changes: 9 additions & 4 deletions packages/console/src/consts/quota-item-phrases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import { type TFuncKey } from 'i18next';

import { type LogtoSkuQuota } from '@/types/skus';

type SkuQuotaItemPhraseKey = Exclude<keyof LogtoSkuQuota, 'inlineHooksEnabled'>;

export const isSkuQuotaItemPhraseKey = (key: keyof LogtoSkuQuota): key is SkuQuotaItemPhraseKey =>
key !== 'inlineHooksEnabled';

/* === for new pricing model === */
export const skuQuotaItemPhrasesMap: Record<
keyof LogtoSkuQuota,
SkuQuotaItemPhraseKey,
TFuncKey<'translation', 'admin_console.subscription.quota_item'>
> = {
mauLimit: 'mau_limit.name',
Expand Down Expand Up @@ -37,7 +42,7 @@ export const skuQuotaItemPhrasesMap: Record<
};

export const skuQuotaItemUnlimitedPhrasesMap: Record<
keyof LogtoSkuQuota,
SkuQuotaItemPhraseKey,
TFuncKey<'translation', 'admin_console.subscription.quota_item'>
> = {
mauLimit: 'mau_limit.unlimited',
Expand Down Expand Up @@ -70,7 +75,7 @@ export const skuQuotaItemUnlimitedPhrasesMap: Record<
};

export const skuQuotaItemLimitedPhrasesMap: Record<
keyof LogtoSkuQuota,
SkuQuotaItemPhraseKey,
TFuncKey<'translation', 'admin_console.subscription.quota_item'>
> = {
mauLimit: 'mau_limit.limited',
Expand Down Expand Up @@ -103,7 +108,7 @@ export const skuQuotaItemLimitedPhrasesMap: Record<
};

export const skuQuotaItemNotEligiblePhrasesMap: Record<
keyof LogtoSkuQuota,
SkuQuotaItemPhraseKey,
TFuncKey<'translation', 'admin_console.subscription.quota_item'>
> = {
mauLimit: 'mau_limit.not_eligible',
Expand Down
5 changes: 5 additions & 0 deletions packages/console/src/consts/tenants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,15 @@ export const defaultLogtoSku: LogtoSkuResponse = {
thirdPartyApplicationsLimit: null,
tenantMembersLimit: 20,
customJwtEnabled: true,
inlineHooksEnabled: false,
subjectTokenEnabled: true,
bringYourUiEnabled: true,
collectUserProfileEnabled: true,
passkeySignInEnabled: true,
idpInitiatedSsoEnabled: false,
samlApplicationsLimit: 3,
securityFeaturesEnabled: true,
customDomainsLimit: 2,
},
};

Expand All @@ -114,6 +117,7 @@ export const defaultSubscriptionQuota: SubscriptionQuota = {
thirdPartyApplicationsLimit: 0,
tenantMembersLimit: 1,
customJwtEnabled: false,
inlineHooksEnabled: false,
subjectTokenEnabled: false,
bringYourUiEnabled: false,
collectUserProfileEnabled: false,
Expand All @@ -140,6 +144,7 @@ export const defaultSubscriptionUsage: SubscriptionCountBasedUsage = {
thirdPartyApplicationsLimit: 0,
tenantMembersLimit: 0,
customJwtEnabled: false,
inlineHooksEnabled: false,
bringYourUiEnabled: false,
collectUserProfileEnabled: false,
passkeySignInEnabled: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { useContext, useEffect, useMemo } from 'react';
import useSWR from 'swr';

import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
import { type LogtoSkuResponse, type SubscriptionUsageResponse } from '@/cloud/types/router';
import {
type LogtoSkuResponse,
type SubscriptionCountBasedUsage,
type SubscriptionQuota,
type SubscriptionUsageResponse,
} from '@/cloud/types/router';
import {
defaultLogtoSku,
defaultTenantResponse,
Expand All @@ -19,6 +24,20 @@ import useSubscription from '../../hooks/use-subscription';

import { type SubscriptionContext } from './types';

const normalizeSubscriptionQuota = (
quota?: SubscriptionUsageResponse['quota']
): SubscriptionQuota => ({
...defaultSubscriptionQuota,
...quota,
});

const normalizeSubscriptionUsage = (
usage?: SubscriptionUsageResponse['usage']
): SubscriptionCountBasedUsage => ({
...defaultSubscriptionUsage,
...usage,
});

const useSubscriptionData: () => SubscriptionContext & { isLoading: boolean } = () => {
const cloudApi = useCloudApi();

Expand Down Expand Up @@ -58,6 +77,21 @@ const useSubscriptionData: () => SubscriptionContext & { isLoading: boolean } =

const logtoSkus = useMemo(() => formatLogtoSkusResponses(fetchedLogtoSkus), [fetchedLogtoSkus]);

const currentSubscriptionQuota = useMemo(
() => normalizeSubscriptionQuota(subscriptionUsageData?.quota),
[subscriptionUsageData?.quota]
);

const currentSubscriptionBasicQuota = useMemo(
() => normalizeSubscriptionQuota(subscriptionUsageData?.basicQuota),
[subscriptionUsageData?.basicQuota]
);

const currentSubscriptionUsage = useMemo(
() => normalizeSubscriptionUsage(subscriptionUsageData?.usage),
[subscriptionUsageData?.usage]
);

const currentSku = useMemo(
() => logtoSkus.find((logtoSku) => logtoSku.id === currentTenant?.planId) ?? defaultLogtoSku,
[currentTenant?.planId, logtoSkus]
Expand All @@ -79,26 +113,26 @@ const useSubscriptionData: () => SubscriptionContext & { isLoading: boolean } =
currentSubscription: currentSubscription ?? defaultTenantResponse.subscription,
onCurrentSubscriptionUpdated: mutateSubscription,
mutateSubscriptionQuotaAndUsages,
currentSubscriptionQuota: subscriptionUsageData?.quota ?? defaultSubscriptionQuota,
currentSubscriptionBasicQuota: subscriptionUsageData?.basicQuota ?? defaultSubscriptionQuota,
currentSubscriptionUsage: subscriptionUsageData?.usage ?? defaultSubscriptionUsage,
currentSubscriptionQuota,
currentSubscriptionBasicQuota,
currentSubscriptionUsage,
currentSubscriptionResourceScopeUsage: subscriptionUsageData?.resources ?? {},
currentSubscriptionRoleScopeUsage: subscriptionUsageData?.roles ?? {},
}),
[
currentSku,
currentSubscription,
currentSubscriptionBasicQuota,
currentSubscriptionQuota,
currentSubscriptionUsage,
isLogtoSkusLoading,
isSubscriptionLoading,
isSubscriptionUsageDataLoading,
logtoSkus,
mutateSubscription,
mutateSubscriptionQuotaAndUsages,
subscriptionUsageData?.quota,
subscriptionUsageData?.basicQuota,
subscriptionUsageData?.resources,
subscriptionUsageData?.roles,
subscriptionUsageData?.usage,
]
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
skuQuotaItemUnlimitedPhrasesMap,
skuQuotaItemPhrasesMap,
skuQuotaItemLimitedPhrasesMap,
isSkuQuotaItemPhraseKey,
} from '@/consts/quota-item-phrases';
import DynamicT from '@/ds-components/DynamicT';
import { type LogtoSkuQuota } from '@/types/skus';
Expand All @@ -16,6 +17,10 @@ type Props = {
};

function SkuQuotaItemPhrase({ skuQuotaKey, skuQuotaValue }: Props) {
if (!isSkuQuotaItemPhraseKey(skuQuotaKey)) {
return null;
}

const isUnlimited = skuQuotaValue === null;
const isNotCapable = skuQuotaValue === 0 || skuQuotaValue === false;
const isLimited = Boolean(skuQuotaValue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
import SkuName from '@/components/SkuName';
import { skuQuotaItemOrder } from '@/consts/plan-quotas';
import {
isSkuQuotaItemPhraseKey,
skuQuotaItemLimitedPhrasesMap,
skuQuotaItemNotEligiblePhrasesMap,
} from '@/consts/quota-item-phrases';
Expand All @@ -19,6 +20,7 @@ import styles from './index.module.scss';

const excludedSkuQuotaKeys = new Set<keyof LogtoSkuQuota>([
'auditLogsRetentionDays',
'inlineHooksEnabled',
'ticketSupportResponseTime',
]);

Expand Down Expand Up @@ -70,6 +72,7 @@ export function NotEligibleSwitchSkuModalContent({
{orderedEntries.map(([quotaKey, quotaValue]) => {
if (
excludedSkuQuotaKeys.has(quotaKey) ||
!isSkuQuotaItemPhraseKey(quotaKey) ||
quotaValue === null || // Unlimited items
quotaValue === true // Eligible items
) {
Expand Down
2 changes: 1 addition & 1 deletion packages/console/src/types/skus.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type SubscriptionQuota } from '@/cloud/types/router';

// TODO: This is a copy from `@logto/cloud-models`, make a SSoT for this later
// This is a copy from `@logto/cloud-models`; make a SSoT for this later.
export enum LogtoSkuType {
Basic = 'Basic',
AddOn = 'AddOn',
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
"zod": "3.24.3"
},
"devDependencies": {
"@logto/cloud": "0.2.5-31c9868",
"@logto/cloud": "0.2.5-66a5752",
"@silverhand/eslint-config": "6.0.1",
"@silverhand/ts-config": "6.0.0",
"@types/adm-zip": "^0.5.5",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/__mocks__/cloud-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const mockQuota = {
thirdPartyApplicationsLimit: 0,
tenantMembersLimit: 1,
customJwtEnabled: false,
inlineHooksEnabled: false,
subjectTokenEnabled: false,
bringYourUiEnabled: false,
collectUserProfileEnabled: false,
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/queries/tenant-usage/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export const isSystemUsageKey = (key: UsageKey): key is SystemUsageKey =>
const quotaUsageKeyGuard = z.enum([
...sharedUsageKeyGuard.options,
'customJwtEnabled',
'inlineHooksEnabled',
'subjectTokenEnabled',
'bringYourUiEnabled',
'collectUserProfileEnabled',
Expand Down Expand Up @@ -132,6 +133,7 @@ type BooleanQuotaUsageKey = {

const booleanQuotaUsageKeyGuard = z.enum([
'customJwtEnabled',
'inlineHooksEnabled',
'subjectTokenEnabled',
'bringYourUiEnabled',
'collectUserProfileEnabled',
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/utils/subscription/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,17 @@ export const getTenantSubscription = async (
// All the dates will be converted to the ISO 8601 format after json serialization.
// Convert the dates to ISO 8601 format to match the exact type of the response.
const { currentPeriodStart, currentPeriodEnd, ...rest } = subscription;
const inlineHooksEnabled =
'inlineHooksEnabled' in rest.quota && typeof rest.quota.inlineHooksEnabled === 'boolean'
? rest.quota.inlineHooksEnabled
: false;

return {
...rest,
quota: {
...rest.quota,
inlineHooksEnabled,
},
currentPeriodStart: new Date(currentPeriodStart).toISOString(),
currentPeriodEnd: new Date(currentPeriodEnd).toISOString(),
};
Expand Down
Loading
Loading