Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: logto-io/logto
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 6e2dcb4047a7fb3b546fddac9ad0d3f8636dc6ff
Choose a base ref
..
head repository: logto-io/logto
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: d600ac45dc71e6c443a73617806cb993f719d82b
Choose a head ref
Showing with 304 additions and 331 deletions.
  1. +6 −4 package.json
  2. +3 −5 packages/console/src/components/ApplicationCreation/CreateForm/index.tsx
  3. +11 −0 ...reateTenantModal/SelectTenantPlanModal/SkuCardItem/FeaturedSkuContent/use-featured-sku-content.ts
  4. +2 −3 packages/console/src/components/CreateTenantModal/SelectTenantPlanModal/SkuCardItem/index.tsx
  5. +26 −4 packages/console/src/components/FeatureTag/index.tsx
  6. +3 −2 packages/console/src/components/Guide/GuideCard/index.tsx
  7. +15 −8 packages/console/src/components/MauExceededModal/index.tsx
  8. +1 −0 packages/console/src/components/PlanDescription/index.tsx
  9. +7 −20 packages/console/src/components/PlanUsage/index.tsx
  10. +1 −0 packages/console/src/consts/plan-quotas.ts
  11. +18 −3 packages/console/src/consts/subscriptions.ts
  12. +2 −4 packages/console/src/ds-components/CardTitle/index.tsx
  13. +5 −3 packages/console/src/pages/ApiResources/components/CreateForm/index.tsx
  14. +2 −2 packages/console/src/pages/Applications/components/GuideLibrary/index.tsx
  15. +6 −4 packages/console/src/pages/CustomizeJwt/index.tsx
  16. +6 −6 packages/console/src/pages/EnterpriseSso/SsoCreationModal/index.tsx
  17. +5 −10 packages/console/src/pages/EnterpriseSso/index.tsx
  18. +5 −3 packages/console/src/pages/Mfa/MfaForm/UpsellNotice/index.tsx
  19. +4 −6 packages/console/src/pages/Mfa/PageWrapper/index.tsx
  20. +10 −9 packages/console/src/pages/OrganizationTemplate/index.tsx
  21. +7 −7 packages/console/src/pages/Organizations/CreateOrganizationModal/index.tsx
  22. +5 −5 packages/console/src/pages/Organizations/index.tsx
  23. +2 −2 packages/console/src/pages/SignInExperience/PageContent/Branding/CustomUiForm/index.tsx
  24. +5 −3 ...console/src/pages/TenantSettings/Subscription/CurrentPlan/AddOnUsageChangesNotification/index.tsx
  25. +2 −2 .../console/src/pages/TenantSettings/Subscription/CurrentPlan/MauLimitExceededNotification/index.tsx
  26. +3 −21 packages/console/src/pages/TenantSettings/Subscription/CurrentPlan/index.tsx
  27. +11 −2 packages/console/src/pages/TenantSettings/Subscription/PlanComparisonTable/index.tsx
  28. +67 −32 packages/console/src/pages/TenantSettings/Subscription/SwitchPlanActionBar/index.tsx
  29. +3 −3 packages/console/src/pages/TenantSettings/TenantMembers/InviteMemberModal/index.tsx
  30. +2 −2 packages/console/src/pages/TenantSettings/components/NotEligibleSwitchPlanModalContent/index.tsx
  31. +34 −12 packages/console/src/utils/subscription.ts
  32. +2 −1 packages/core/src/libraries/quota.ts
  33. +0 −2 packages/integration-tests/src/tests/console/user-management.test.ts
  34. +1 −0 packages/phrases/src/locales/ar/translation/admin-console/subscription/quota-table.ts
  35. +1 −0 packages/phrases/src/locales/de/translation/admin-console/subscription/quota-table.ts
  36. +1 −0 packages/phrases/src/locales/en/translation/admin-console/subscription/quota-table.ts
  37. +1 −0 packages/phrases/src/locales/es/translation/admin-console/subscription/quota-table.ts
  38. +1 −0 packages/phrases/src/locales/fr/translation/admin-console/subscription/quota-table.ts
  39. +1 −0 packages/phrases/src/locales/it/translation/admin-console/subscription/quota-table.ts
  40. +1 −0 packages/phrases/src/locales/ja/translation/admin-console/subscription/quota-table.ts
  41. +1 −0 packages/phrases/src/locales/ko/translation/admin-console/subscription/quota-table.ts
  42. +1 −0 packages/phrases/src/locales/pl-pl/translation/admin-console/subscription/quota-table.ts
  43. +1 −0 packages/phrases/src/locales/pt-br/translation/admin-console/subscription/quota-table.ts
  44. +1 −0 packages/phrases/src/locales/pt-pt/translation/admin-console/subscription/quota-table.ts
  45. +1 −0 packages/phrases/src/locales/ru/translation/admin-console/subscription/quota-table.ts
  46. +1 −0 packages/phrases/src/locales/tr-tr/translation/admin-console/subscription/quota-table.ts
  47. +1 −0 packages/phrases/src/locales/zh-cn/translation/admin-console/subscription/quota-table.ts
  48. +1 −0 packages/phrases/src/locales/zh-hk/translation/admin-console/subscription/quota-table.ts
  49. +1 −0 packages/phrases/src/locales/zh-tw/translation/admin-console/subscription/quota-table.ts
  50. +7 −141 pnpm-lock.yaml
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -42,17 +42,19 @@
},
"pnpm": {
"overrides": {
"formidable@<3.2.4": "^3.2.4",
"d3-color@2.0.0": "^3.1.0",
"@75lb/deep-merge@<1.1.2": "^1.1.2",
"braces@<3.0.3": "^3.0.3",
"cross-spawn@<6.0.6": "^6.0.6",
"cross-spawn@>=7.0.0 <7.0.5": "^7.0.5",
"@75lb/deep-merge@<1.1.2": "^1.1.2",
"d3-color@2.0.0": "^3.1.0",
"formidable@<3.2.4": "^3.2.4",
"micromatch@<4.0.8": "^4.0.8",
"nanoid@>=4.0.0 <5.0.9": "^5.0.9",
"path-to-regexp@>=0.2.0 <1.9.0": "^1.9.0",
"path-to-regexp@>=4.0.0 <6.3.0": "^6.3.0",
"rollup@>=4.0.0 <4.22.4": "^4.22.4"
"puppeteer-core@<23.10.3": "^23.10.3",
"rollup@>=4.0.0 <4.22.4": "^4.22.4",
"ws@>=8.0.0 <8.17.1": "^8.18.0"
},
"peerDependencyRules": {
"allowedVersions": {
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type AdminConsoleKey } from '@logto/phrases';
import type { Application } from '@logto/schemas';
import { ApplicationType, ReservedPlanId } from '@logto/schemas';
import { ApplicationType } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { type ReactElement, useContext, useMemo } from 'react';
import { useController, useForm } from 'react-hook-form';
@@ -11,6 +11,7 @@ import { useSWRConfig } from 'swr';

import { GtagConversionId, reportConversion } from '@/components/Conversion/utils';
import { isDevFeaturesEnabled } from '@/consts/env';
import { latestProPlanId } from '@/consts/subscriptions';
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
import DynamicT from '@/ds-components/DynamicT';
import FormField from '@/ds-components/FormField';
@@ -142,10 +143,7 @@ function CreateForm({
title="applications.create"
subtitle={subtitleElement}
paywall={conditional(
isPaidTenant &&
watch('type') === ApplicationType.MachineToMachine &&
planId !== ReservedPlanId.Pro &&
ReservedPlanId.Pro
!isPaidTenant && watch('type') === ApplicationType.MachineToMachine && latestProPlanId
)}
hasAddOnTag={
isPaidTenant &&
Original file line number Diff line number Diff line change
@@ -10,13 +10,24 @@ import {
freePlanPermissionsLimit,
freePlanRoleLimit,
proPlanAuditLogsRetentionDays,
// eslint-disable-next-line unused-imports/no-unused-imports -- for jsdoc usage
featuredPlanIds,
} from '@/consts/subscriptions';

type ContentData = {
readonly title: string;
readonly isAvailable: boolean;
};

/**
* This hook is used to build the plan content on the SelectTenantPlanModal.
* It is used to display the features of the selected plan.
* Currently, all the feature content is hardcoded.
* For the grandfathered Pro plan and new created Pro202411 plan, the content is the same.
* So we don't need to differentiate them. here.
*
* @param skuId The selected sku id. Can only be one of {@link featuredPlanIds}
*/
const useFeaturedSkuContent = (skuId: string) => {
const { t } = useTranslation(undefined, {
keyPrefix: 'admin_console.upsell.featured_plan_content',
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ import DangerousRaw from '@/ds-components/DangerousRaw';
import DynamicT from '@/ds-components/DynamicT';
import FlipOnRtl from '@/ds-components/FlipOnRtl';
import TextLink from '@/ds-components/TextLink';
import { isProPlan } from '@/utils/subscription';

import FeaturedSkuContent from './FeaturedSkuContent';
import styles from './index.module.scss';
@@ -92,9 +93,7 @@ function SkuCardItem({ sku, onSelect, buttonProps }: Props) {
disabled={(isFreeSku && isFreeTenantExceeded) || buttonProps?.disabled}
/>
</div>
{skuId === ReservedPlanId.Pro && (
<div className={styles.mostPopularTag}>{t('most_popular')}</div>
)}
{isProPlan(skuId) && <div className={styles.mostPopularTag}>{t('most_popular')}</div>}
</div>
);
}
30 changes: 26 additions & 4 deletions packages/console/src/components/FeatureTag/index.tsx
Original file line number Diff line number Diff line change
@@ -5,20 +5,42 @@ import { useContext } from 'react';
import { isCloud } from '@/consts/env';
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
import { TenantsContext } from '@/contexts/TenantsProvider';
import { isProPlan } from '@/utils/subscription';

import styles from './index.module.scss';

export { default as BetaTag } from './BetaTag';

/**
* The display tag mapping for each ReservedPlanId.
*/
const planIdTagMap: Record<ReservedPlanId, string> = {
[ReservedPlanId.Free]: 'free',
[ReservedPlanId.Pro]: 'pro',
[ReservedPlanId.Pro202411]: 'pro',
[ReservedPlanId.Development]: 'dev',
[ReservedPlanId.Admin]: 'admin',
};

/**
* The minimum plan required to use the feature.
* Currently we only have pro plan paywall.
*/
export type PaywallPlanId = Extract<ReservedPlanId, ReservedPlanId.Pro | ReservedPlanId.Pro202411>;

export type Props = {
/**
* Whether the tag should be visible. It should be `true` if the tenant's subscription
* plan has NO access to the feature (paywall), but it will always be visible for dev
* tenants.
*/
readonly isVisible: boolean;
/** The minimum plan required to use the feature. */
readonly plan: Exclude<ReservedPlanId, ReservedPlanId.Free | ReservedPlanId.Development>;
/**
* The minimum plan required to use the feature.
* Currently we only have pro plan paywall.
* Set the default value to the latest pro plan id we are using.
*/
readonly plan: PaywallPlanId;
readonly className?: string;
};

@@ -61,7 +83,7 @@ function FeatureTag(props: Props) {
return null;
}

return <div className={classNames(styles.tag, className)}>{plan}</div>;
return <div className={classNames(styles.tag, className)}>{planIdTagMap[plan]}</div>;
}

export default FeatureTag;
@@ -89,7 +111,7 @@ export function CombinedAddOnAndFeatureTag(props: CombinedAddOnAndFeatureTagProp
}

// Show the "Add-on" tag for Pro plan.
if (hasAddOnTag && isCloud && planId === ReservedPlanId.Pro) {
if (hasAddOnTag && isCloud && isProPlan(planId)) {
return (
<div className={classNames(styles.tag, styles.beta, styles.addOn, className)}>Add-on</div>
);
5 changes: 3 additions & 2 deletions packages/console/src/components/Guide/GuideCard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { ReservedPlanId, Theme } from '@logto/schemas';
import { Theme } from '@logto/schemas';
import classNames from 'classnames';
import { Suspense, useCallback } from 'react';

import { type Guide, type GuideMetadata } from '@/assets/docs/guides/types';
import FeatureTag, { BetaTag } from '@/components/FeatureTag';
import { latestProPlanId } from '@/consts/subscriptions';
import Button from '@/ds-components/Button';
import useTheme from '@/hooks/use-theme';
import { onKeyDownHandler } from '@/utils/a11y';
@@ -61,7 +62,7 @@ function GuideCard({ data, onClick, hasBorder, hasButton, hasPaywall, isBeta }:
<div className={styles.name}>{name}</div>
{hasTags && (
<div className={styles.tagWrapper}>
{hasPaywall && <FeatureTag isVisible plan={ReservedPlanId.Pro} />}
{hasPaywall && <FeatureTag isVisible plan={latestProPlanId} />}
{isBeta && <BetaTag />}
</div>
)}
23 changes: 15 additions & 8 deletions packages/console/src/components/MauExceededModal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { cond } from '@silverhand/essentials';
import { useContext, useState } from 'react';
import { conditional } from '@silverhand/essentials';
import { useContext, useMemo, useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import ReactModal from 'react-modal';

@@ -33,18 +33,25 @@ function MauExceededModal() {
setHasClosed(true);
};

if (hasClosed) {
return null;
}
const periodicUsage = useMemo(
() =>
conditional(
currentTenant && {
mauLimit: currentTenant.usage.activeUsers,
tokenLimit: currentTenant.usage.tokenUsage,
}
),
[currentTenant]
);

const isMauExceeded = cond(
const isMauExceeded = conditional(
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
currentTenant &&
currentTenant.quota.mauLimit !== null &&
currentTenant.usage.activeUsers >= currentTenant.quota.mauLimit
);

if (!isMauExceeded) {
if (hasClosed || !isMauExceeded) {
return null;
}

@@ -85,7 +92,7 @@ function MauExceededModal() {
</Trans>
</InlineNotification>
<FormField title="subscription.plan_usage">
<PlanUsage />
<PlanUsage periodicUsage={periodicUsage} />
</FormField>
</ModalLayout>
</ReactModal>
1 change: 1 addition & 0 deletions packages/console/src/components/PlanDescription/index.tsx
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ const registeredPlanDescriptionPhrasesMap: Record<
> = {
[ReservedPlanId.Free]: 'free_plan_description',
[ReservedPlanId.Pro]: 'pro_plan_description',
[ReservedPlanId.Pro202411]: 'pro_plan_description',
};

const getRegisteredPlanDescriptionPhrase = (
27 changes: 7 additions & 20 deletions packages/console/src/components/PlanUsage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { ReservedPlanId } from '@logto/schemas';
import { cond, conditional } from '@silverhand/essentials';
import { cond } from '@silverhand/essentials';
import classNames from 'classnames';
import dayjs from 'dayjs';
import { useContext, useMemo } from 'react';
import { useContext } from 'react';

import {
type NewSubscriptionPeriodicUsage,
type NewSubscriptionCountBasedUsage,
type NewSubscriptionQuota,
} from '@/cloud/types/router';
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
import { TenantsContext } from '@/contexts/TenantsProvider';
import DynamicT from '@/ds-components/DynamicT';
import { formatPeriod, isPaidPlan } from '@/utils/subscription';
import { formatPeriod, isPaidPlan, isProPlan } from '@/utils/subscription';

import PlanUsageCard, { type Props as PlanUsageCardProps } from './PlanUsageCard';
import styles from './index.module.scss';
@@ -26,7 +25,7 @@ import {
} from './utils';

type Props = {
readonly periodicUsage?: NewSubscriptionPeriodicUsage;
readonly periodicUsage: NewSubscriptionPeriodicUsage | undefined;
};

const getUsageByKey = (
@@ -58,26 +57,13 @@ const getUsageByKey = (
return countBasedUsage[key];
};

function PlanUsage({ periodicUsage: rawPeriodicUsage }: Props) {
function PlanUsage({ periodicUsage }: Props) {
const {
currentSubscriptionQuota,
currentSubscriptionBasicQuota,
currentSubscriptionUsage,
currentSubscription: { currentPeriodStart, currentPeriodEnd, planId, isEnterprisePlan },
} = useContext(SubscriptionDataContext);
const { currentTenant } = useContext(TenantsContext);

const periodicUsage = useMemo(
() =>
rawPeriodicUsage ??
conditional(
currentTenant && {
mauLimit: currentTenant.usage.activeUsers,
tokenLimit: currentTenant.usage.tokenUsage,
}
),
[currentTenant, rawPeriodicUsage]
);

if (!periodicUsage) {
return null;
@@ -102,6 +88,7 @@ function PlanUsage({ periodicUsage: rawPeriodicUsage }: Props) {
titleKey: `subscription.usage.${titleKeyMap[key]}`,
unitPrice: usageKeyPriceMap[key],
...cond(
// We only show the usage card for MAU and token for Free plan
(key === 'tokenLimit' || key === 'mauLimit' || isPaidTenant) && {
quota: currentSubscriptionQuota[key],
}
@@ -134,7 +121,7 @@ function PlanUsage({ periodicUsage: rawPeriodicUsage }: Props) {
// Hide the quota notice for Pro plans if the basic quota is 0.
// Per current pricing model design, it should apply to `enterpriseSsoLimit`.
...cond(
planId === ReservedPlanId.Pro &&
isProPlan(planId) &&
currentSubscriptionBasicQuota[key] === 0 && {
isQuotaNoticeHidden: true,
}
1 change: 1 addition & 0 deletions packages/console/src/consts/plan-quotas.ts
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import { type LogtoSkuQuota } from '@/types/skus';
export const ticketSupportResponseTimeMap: Record<string, number> = {
[ReservedPlanId.Free]: 0,
[ReservedPlanId.Pro]: 48,
[ReservedPlanId.Pro202411]: 48,
};

/**
21 changes: 18 additions & 3 deletions packages/console/src/consts/subscriptions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ReservedPlanId } from '@logto/schemas';

import { isDevFeaturesEnabled } from './env';

/**
* Shared quota limits between the featured plan content in the `CreateTenantModal` and the `PlanComparisonTable`.
*/
@@ -23,14 +25,27 @@ export const tokenAddOnUnitPrice = 80;
export const hooksAddOnUnitPrice = 2;
/* === Add-on unit price (in USD) === */

// TODO: Remove this dev feature flag when we have the new Pro202411 plan released.
/**
* In console, only featured plans are shown in the plan selection component.
* we will this to filter out the public visible featured plans.
*/
export const featuredPlanIds: string[] = [ReservedPlanId.Free, ReservedPlanId.Pro];
export const featuredPlanIds: readonly string[] = isDevFeaturesEnabled
? Object.freeze([ReservedPlanId.Free, ReservedPlanId.Pro202411])
: Object.freeze([ReservedPlanId.Free, ReservedPlanId.Pro]);

/**
* The order of featured plans in the plan selection content component.
* The order of plans in the plan selection content component.
* Unlike the `featuredPlanIds`, include both grandfathered plans and public visible featured plans.
* We need to properly identify the order of the grandfathered plans compared to the new public visible featured plans.
*/
export const featuredPlanIdOrder: string[] = [ReservedPlanId.Free, ReservedPlanId.Pro];
export const planIdOrder: Record<string, number> = Object.freeze({
[ReservedPlanId.Free]: 0,
[ReservedPlanId.Pro]: 1,
[ReservedPlanId.Pro202411]: 1,
});

export const checkoutStateQueryKey = 'checkout-state';

/** The latest pro plan id we are using. TODO: Remove this when we have the new Pro202411 plan released. */
export const latestProPlanId = isDevFeaturesEnabled ? ReservedPlanId.Pro202411 : ReservedPlanId.Pro;
6 changes: 2 additions & 4 deletions packages/console/src/ds-components/CardTitle/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import type { AdminConsoleKey } from '@logto/phrases';
import { type ReservedPlanId } from '@logto/schemas';
import classNames from 'classnames';
import type { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';

import { CombinedAddOnAndFeatureTag } from '@/components/FeatureTag';
import { CombinedAddOnAndFeatureTag, type PaywallPlanId } from '@/components/FeatureTag';
import type { Props as TextLinkProps } from '@/ds-components/TextLink';

import type DangerousRaw from '../DangerousRaw';
@@ -22,10 +21,9 @@ export type Props = {
readonly className?: string;
/**
* If a paywall tag should be shown next to the title. The value is the plan type.
*
* If not provided, no paywall tag will be shown.
*/
readonly paywall?: Exclude<ReservedPlanId, ReservedPlanId.Free | ReservedPlanId.Development>;
readonly paywall?: PaywallPlanId;
readonly hasAddOnTag?: boolean;
};

Loading