diff --git a/packages/console/src/pages/Roles/components/CreateRoleForm/Footer.tsx b/packages/console/src/pages/Roles/components/CreateRoleForm/Footer.tsx
new file mode 100644
index 000000000..399844af7
--- /dev/null
+++ b/packages/console/src/pages/Roles/components/CreateRoleForm/Footer.tsx
@@ -0,0 +1,98 @@
+import { type RoleResponse, RoleType, ReservedPlanId } from '@logto/schemas';
+import { useContext } from 'react';
+import { Trans, useTranslation } from 'react-i18next';
+import useSWR from 'swr';
+
+import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
+import PlanName from '@/components/PlanName';
+import QuotaGuardFooter from '@/components/QuotaGuardFooter';
+import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
+import { TenantsContext } from '@/contexts/TenantsProvider';
+import Button from '@/ds-components/Button';
+import useSubscriptionPlan from '@/hooks/use-subscription-plan';
+import { hasReachedQuotaLimit } from '@/utils/quota';
+import { buildUrl } from '@/utils/url';
+
+type Props = {
+ roleType: RoleType;
+ selectedScopesCount: number;
+ isCreating: boolean;
+ onClickCreate: () => void;
+};
+
+function Footer({ roleType, selectedScopesCount, isCreating, onClickCreate }: Props) {
+ const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
+ const { currentTenantId } = useContext(TenantsContext);
+ const { data: currentPlan } = useSubscriptionPlan(currentTenantId);
+ const { data: [, roleCount] = [] } = useSWR<[RoleResponse[], number]>(
+ isCloud &&
+ buildUrl('api/roles', {
+ page: String(1),
+ page_size: String(1),
+ type: roleType,
+ })
+ );
+
+ const hasRoleReachedLimit = hasReachedQuotaLimit({
+ quotaKey: roleType === RoleType.User ? 'rolesLimit' : 'machineToMachineRolesLimit',
+ plan: currentPlan,
+ usage: roleCount ?? 0,
+ });
+
+ const hasBeyondScopesPerRoleLimit = hasReachedQuotaLimit({
+ quotaKey: 'scopesPerRoleLimit',
+ plan: currentPlan,
+ /**
+ * If usage equals to the limit, it means the current role has reached the maximum allowed scope.
+ * Therefore, we should not assign any more scopes at this point.
+ * However, the currently selected scopes haven't been assigned yet, so we subtract 1
+ * to allow the assignment when the scope count equals to the limit.
+ */
+ usage: selectedScopesCount - 1,
+ });
+
+ if (currentPlan && (hasRoleReachedLimit || hasBeyondScopesPerRoleLimit)) {
+ return (
+
+ ,
+ planName: ,
+ }}
+ >
+ {/* User roles limit paywall */}
+ {hasRoleReachedLimit &&
+ roleType === RoleType.User &&
+ t('upsell.paywall.roles', { count: currentPlan.quota.rolesLimit ?? 0 })}
+ {hasRoleReachedLimit &&
+ roleType === RoleType.MachineToMachine &&
+ /* Todo @xiaoyijun [Pricing] Remove feature flag */
+ (!isDevFeaturesEnabled && currentPlan.id === ReservedPlanId.Free
+ ? t('upsell.paywall.deprecated_machine_to_machine_feature')
+ : t('upsell.paywall.machine_to_machine_roles', {
+ count: currentPlan.quota.machineToMachineRolesLimit ?? 0,
+ }))}
+ {/* Role scopes limit paywall */}
+ {!hasRoleReachedLimit &&
+ hasBeyondScopesPerRoleLimit &&
+ t('upsell.paywall.scopes_per_role', {
+ count: currentPlan.quota.scopesPerRoleLimit ?? 0,
+ })}
+
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+export default Footer;
diff --git a/packages/console/src/pages/Roles/components/CreateRoleForm/index.tsx b/packages/console/src/pages/Roles/components/CreateRoleForm/index.tsx
index ce521635f..47e575649 100644
--- a/packages/console/src/pages/Roles/components/CreateRoleForm/index.tsx
+++ b/packages/console/src/pages/Roles/components/CreateRoleForm/index.tsx
@@ -4,14 +4,11 @@ import { ReservedPlanId, RoleType, internalRolePrefix } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useContext, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
-import { Trans, useTranslation } from 'react-i18next';
+import { useTranslation } from 'react-i18next';
import KeyboardArrowDown from '@/assets/icons/keyboard-arrow-down.svg';
import KeyboardArrowUp from '@/assets/icons/keyboard-arrow-up.svg';
-import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
import FeatureTag from '@/components/FeatureTag';
-import PlanName from '@/components/PlanName';
-import QuotaGuardFooter from '@/components/QuotaGuardFooter';
import RoleScopesTransfer from '@/components/RoleScopesTransfer';
import { isDevFeaturesEnabled } from '@/consts/env';
import { TenantsContext } from '@/contexts/TenantsProvider';
@@ -23,10 +20,9 @@ import RadioGroup, { Radio } from '@/ds-components/RadioGroup';
import TextInput from '@/ds-components/TextInput';
import useApi from '@/hooks/use-api';
import useSubscriptionPlan from '@/hooks/use-subscription-plan';
-import { ReservedPlanName } from '@/types/subscriptions';
import { trySubmitSafe } from '@/utils/form';
-import { hasReachedQuotaLimit } from '@/utils/quota';
+import Footer from './Footer';
import * as styles from './index.module.scss';
type RadioOption = { key: AdminConsoleKey; value: RoleType; hasPaywall: boolean };
@@ -37,7 +33,6 @@ const radioOptions: RadioOption[] = [
];
export type Props = {
- totalRoleCount: number;
onClose: (createdRole?: Role) => void;
};
@@ -49,7 +44,7 @@ type CreateRolePayload = Pick & {
scopeIds?: string[];
};
-function CreateRoleForm({ totalRoleCount, onClose }: Props) {
+function CreateRoleForm({ onClose }: Props) {
const { currentTenantId } = useContext(TenantsContext);
const [isTypeSelectorVisible, setIsTypeSelectorVisible] = useState(false);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
@@ -63,7 +58,6 @@ function CreateRoleForm({ totalRoleCount, onClose }: Props) {
} = useForm({ defaultValues: { type: RoleType.User } });
const api = useApi();
- const roleScopes = watch('scopes', []);
const onSubmit = handleSubmit(
trySubmitSafe(async ({ name, description, type, scopes }) => {
@@ -83,24 +77,6 @@ function CreateRoleForm({ totalRoleCount, onClose }: Props) {
})
);
- const isRolesReachLimit = hasReachedQuotaLimit({
- quotaKey: 'rolesLimit',
- plan: currentPlan,
- usage: totalRoleCount,
- });
-
- const isScopesPerReachLimit = hasReachedQuotaLimit({
- quotaKey: 'scopesPerRoleLimit',
- plan: currentPlan,
- /**
- * If usage is equal to the limit, it means the current role has reached the maximum allowed scope.
- * Therefore, we should not assign any more scopes at this point.
- * However, the currently selected scopes haven't been assigned yet, so we subtract 1
- * to allow the assignment when the scope count is equal to the limit.
- */
- usage: roleScopes.length - 1,
- });
-
return (
{
- if (
- currentPlan?.name === ReservedPlanName.Free &&
- watch('type') === RoleType.MachineToMachine
- ) {
- return (
-
- ,
- }}
- >
- {t('upsell.paywall.deprecated_machine_to_machine_feature')}
-
-
- );
- }
- if (isRolesReachLimit && currentPlan) {
- return (
-
- ,
- planName: ,
- }}
- >
- {t('upsell.paywall.roles', { count: currentPlan.quota.rolesLimit ?? 0 })}
-
-
- );
- }
- if (isScopesPerReachLimit && currentPlan && !isRolesReachLimit) {
- return (
-
- ,
- planName: ,
- }}
- >
- {t('upsell.paywall.scopes_per_role', {
- count: currentPlan.quota.scopesPerRoleLimit ?? 0,
- })}
-
-
- );
- }
- if (!isRolesReachLimit && !isScopesPerReachLimit) {
- return (
-
- );
- }
- })()}
+ footer={
+
+ }
onClose={onClose}
>