mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
feat(console,core,phrases): add more organization paywalls (#4952)
This commit is contained in:
parent
979bf08966
commit
9a91c0ad10
21 changed files with 96 additions and 5 deletions
|
@ -1,14 +1,19 @@
|
|||
import { type Organization, type CreateOrganization } from '@logto/schemas';
|
||||
import { useEffect } from 'react';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import ReactModal from 'react-modal';
|
||||
|
||||
import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
|
||||
import QuotaGuardFooter from '@/components/QuotaGuardFooter';
|
||||
import { isCloud } from '@/consts/env';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
import ModalLayout from '@/ds-components/ModalLayout';
|
||||
import TextInput from '@/ds-components/TextInput';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import useSubscriptionPlan from '@/hooks/use-subscription-plan';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
import { trySubmitSafe } from '@/utils/form';
|
||||
|
||||
|
@ -20,6 +25,10 @@ type Props = {
|
|||
function CreateOrganizationModal({ isOpen, onClose }: Props) {
|
||||
const api = useApi();
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { currentTenantId } = useContext(TenantsContext);
|
||||
const { data: currentPlan } = useSubscriptionPlan(currentTenantId);
|
||||
const isOrganizationsDisabled = isCloud && !currentPlan?.quota.organizationsEnabled;
|
||||
|
||||
const {
|
||||
reset,
|
||||
register,
|
||||
|
@ -56,7 +65,24 @@ function CreateOrganizationModal({ isOpen, onClose }: Props) {
|
|||
<ModalLayout
|
||||
title="organizations.create_organization"
|
||||
footer={
|
||||
<Button type="primary" title="general.create" isLoading={isSubmitting} onClick={submit} />
|
||||
isOrganizationsDisabled ? (
|
||||
<QuotaGuardFooter>
|
||||
<Trans
|
||||
components={{
|
||||
a: <ContactUsPhraseLink />,
|
||||
}}
|
||||
>
|
||||
{t('upsell.paywall.organizations')}
|
||||
</Trans>
|
||||
</QuotaGuardFooter>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
title="general.create"
|
||||
isLoading={isSubmitting}
|
||||
onClick={submit}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onClose={onClose}
|
||||
>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import Plus from '@/assets/icons/plus.svg';
|
||||
import PageMeta from '@/components/PageMeta';
|
||||
import { isCloud } from '@/consts/env';
|
||||
import { subscriptionPage } from '@/consts/pages';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
|
@ -40,7 +41,7 @@ function Organizations({ tab }: Props) {
|
|||
const [isCreating, setIsCreating] = useState(false);
|
||||
const { configs, isLoading: isLoadingConfigs } = useConfigs();
|
||||
const isInitialSetup = !isLoadingConfigs && !configs?.organizationCreated;
|
||||
const isOrganizationsDisabled = currentPlan?.id === ReservedPlanId.Free;
|
||||
const isOrganizationsDisabled = isCloud && !currentPlan?.quota.organizationsEnabled;
|
||||
|
||||
const upgradePlan = useCallback(() => {
|
||||
navigate(subscriptionPage);
|
||||
|
|
|
@ -3,17 +3,25 @@ import type { MiddlewareType } from 'koa';
|
|||
import { type QuotaLibrary } from '#src/libraries/quota.js';
|
||||
import { type FeatureQuota } from '#src/utils/subscription/types.js';
|
||||
|
||||
type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'COPY' | 'HEAD' | 'OPTIONS';
|
||||
|
||||
type UsageGuardConfig = {
|
||||
key: keyof FeatureQuota;
|
||||
quota: QuotaLibrary;
|
||||
/** Guard usage only for the specified method types. Guard all if not provided. */
|
||||
methods?: Method[];
|
||||
};
|
||||
|
||||
export default function koaQuotaGuard<StateT, ContextT, ResponseBodyT>({
|
||||
key,
|
||||
quota,
|
||||
methods,
|
||||
}: UsageGuardConfig): MiddlewareType<StateT, ContextT, ResponseBodyT> {
|
||||
return async (ctx, next) => {
|
||||
await quota.guardKey(key);
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
if (!methods || methods.includes(ctx.method.toUpperCase() as Method)) {
|
||||
await quota.guardKey(key);
|
||||
}
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { z } from 'zod';
|
|||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import koaPagination from '#src/middleware/koa-pagination.js';
|
||||
import koaQuotaGuard from '#src/middleware/koa-quota-guard.js';
|
||||
import { userSearchKeys } from '#src/queries/user.js';
|
||||
import SchemaRouter from '#src/utils/SchemaRouter.js';
|
||||
import { parseSearchOptions } from '#src/utils/search.js';
|
||||
|
@ -26,6 +27,7 @@ export default function organizationRoutes<T extends AuthedRouter>(...args: Rout
|
|||
originalRouter,
|
||||
{
|
||||
queries: { organizations },
|
||||
libraries: { quota },
|
||||
},
|
||||
] = args;
|
||||
|
||||
|
@ -235,6 +237,8 @@ export default function organizationRoutes<T extends AuthedRouter>(...args: Rout
|
|||
organizationRoleRoutes(...args);
|
||||
organizationScopeRoutes(...args);
|
||||
|
||||
router.use(koaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] }));
|
||||
|
||||
// Add routes to the router
|
||||
originalRouter.use(router.routes());
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { z } from 'zod';
|
|||
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import koaPagination from '#src/middleware/koa-pagination.js';
|
||||
import koaQuotaGuard from '#src/middleware/koa-quota-guard.js';
|
||||
import SchemaRouter from '#src/utils/SchemaRouter.js';
|
||||
|
||||
import { type AuthedRouter, type RouterInitArgs } from '../types.js';
|
||||
|
@ -24,6 +25,7 @@ export default function organizationRoleRoutes<T extends AuthedRouter>(
|
|||
relations: { rolesScopes },
|
||||
},
|
||||
},
|
||||
libraries: { quota },
|
||||
},
|
||||
]: RouterInitArgs<T>
|
||||
) {
|
||||
|
@ -87,5 +89,7 @@ export default function organizationRoleRoutes<T extends AuthedRouter>(
|
|||
|
||||
router.addRelationRoutes(rolesScopes, 'scopes');
|
||||
|
||||
router.use(koaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] }));
|
||||
|
||||
originalRouter.use(router.routes());
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { OrganizationScopes } from '@logto/schemas';
|
||||
|
||||
import koaQuotaGuard from '#src/middleware/koa-quota-guard.js';
|
||||
import SchemaRouter from '#src/utils/SchemaRouter.js';
|
||||
|
||||
import { type AuthedRouter, type RouterInitArgs } from '../types.js';
|
||||
|
@ -13,6 +14,7 @@ export default function organizationScopeRoutes<T extends AuthedRouter>(
|
|||
queries: {
|
||||
organizations: { scopes },
|
||||
},
|
||||
libraries: { quota },
|
||||
},
|
||||
]: RouterInitArgs<T>
|
||||
) {
|
||||
|
@ -21,5 +23,7 @@ export default function organizationScopeRoutes<T extends AuthedRouter>(
|
|||
searchFields: ['name'],
|
||||
});
|
||||
|
||||
router.use(koaQuotaGuard({ key: 'organizationsEnabled', quota, methods: ['POST', 'PUT'] }));
|
||||
|
||||
originalRouter.use(router.routes());
|
||||
}
|
||||
|
|
|
@ -46,6 +46,9 @@ const paywall = {
|
|||
hooks_other:
|
||||
'Sie haben das Limit von {{count, number}} <planName/>-Webhooks erreicht. Upgraden Sie Ihren Plan, um mehr Webhooks zu erstellen. Zögern Sie nicht, <a>Kontaktieren Sie uns</a>, wenn Sie Hilfe benötigen.',
|
||||
mfa: 'Schalten Sie MFA zur Sicherheitsüberprüfung frei, indem Sie auf einen kostenpflichtigen Plan aktualisieren. Zögern Sie nicht, uns zu <a>kontaktieren</a>, wenn Sie Unterstützung benötigen.',
|
||||
/** UNTRANSLATED */
|
||||
organizations:
|
||||
'Unlock organizations by upgrading to a paid plan. Don’t hesitate to <a>contact us</a> if you need any assistance.',
|
||||
};
|
||||
|
||||
export default Object.freeze(paywall);
|
||||
|
|
|
@ -46,6 +46,8 @@ const paywall = {
|
|||
hooks_other:
|
||||
'{{count, number}} webhooks of <planName/> limit reached. Upgrade plan to create more webhooks. Feel free to <a>contact us</a> if you need any assistance.',
|
||||
mfa: 'Unlock MFA to verification security by upgrading to a paid plan. Don’t hesitate to <a>contact us</a> if you need any assistance.',
|
||||
organizations:
|
||||
'Unlock organizations by upgrading to a paid plan. Don’t hesitate to <a>contact us</a> if you need any assistance.',
|
||||
};
|
||||
|
||||
export default Object.freeze(paywall);
|
||||
|
|
|
@ -46,6 +46,9 @@ const paywall = {
|
|||
hooks_other:
|
||||
'Has alcanzado el límite de {{count, number}} webhooks de <planName/>. Actualiza el plan para crear más webhooks. Si necesitas ayuda, no dudes en <a>contactarnos</a>.',
|
||||
mfa: 'Desbloquea MFA para verificar la seguridad al actualizar a un plan pago. No dudes en <a>contactarnos</a> si necesitas ayuda.',
|
||||
/** UNTRANSLATED */
|
||||
organizations:
|
||||
'Unlock organizations by upgrading to a paid plan. Don’t hesitate to <a>contact us</a> if you need any assistance.',
|
||||
};
|
||||
|
||||
export default Object.freeze(paywall);
|
||||
|
|
|
@ -46,6 +46,9 @@ const paywall = {
|
|||
hooks_other:
|
||||
"Vous avez atteint la limite de {{count, number}} webhooks de <planName/>. Mettez à niveau votre plan pour créer plus de webhooks. N'hésitez pas à <a>nous contacter</a> si vous avez besoin d'aide.",
|
||||
mfa: "Déverrouillez MFA pour vérifier la sécurité en passant à un plan payant. N'hésitez pas à <a>nous contacter</a> si vous avez besoin d'aide.",
|
||||
/** UNTRANSLATED */
|
||||
organizations:
|
||||
'Unlock organizations by upgrading to a paid plan. Don’t hesitate to <a>contact us</a> if you need any assistance.',
|
||||
};
|
||||
|
||||
export default Object.freeze(paywall);
|
||||
|
|
|
@ -46,6 +46,9 @@ const paywall = {
|
|||
hooks_other:
|
||||
'Hai raggiunto il limite di {{count, number}} webhook di <planName/>. Aggiorna il piano per creare altri webhook. Non esitare a <a>contattarci</a> se hai bisogno di assistenza.',
|
||||
mfa: 'Sblocca MFA per verificare la sicurezza passando a un piano a pagamento. Non esitare a <a>contattarci</a> se hai bisogno di assistenza.',
|
||||
/** UNTRANSLATED */
|
||||
organizations:
|
||||
'Unlock organizations by upgrading to a paid plan. Don’t hesitate to <a>contact us</a> if you need any assistance.',
|
||||
};
|
||||
|
||||
export default Object.freeze(paywall);
|
||||
|
|
|
@ -46,6 +46,9 @@ const paywall = {
|
|||
hooks_other:
|
||||
'{{count, number}}の<planName/>ウェブフック制限に達しました。追加のウェブフックを作成するにはプランをアップグレードしてください。<a>お問い合わせ</a>は何かお手伝いが必要な場合はお気軽にどうぞ。',
|
||||
mfa: 'セキュリティを確認するためにMFAを解除して有料プランにアップグレードしてください。ご質問があれば、<a>お問い合わせください</a>。',
|
||||
/** UNTRANSLATED */
|
||||
organizations:
|
||||
'Unlock organizations by upgrading to a paid plan. Don’t hesitate to <a>contact us</a> if you need any assistance.',
|
||||
};
|
||||
|
||||
export default Object.freeze(paywall);
|
||||
|
|
|
@ -46,6 +46,9 @@ const paywall = {
|
|||
hooks_other:
|
||||
'<planName/>의 {{count, number}}개 웹훅 한도에 도달했습니다. 더 많은 웹훅을 생성하려면 플랜을 업그레이드하세요. 도움이 필요하면 <a>문의하기</a>로 연락 주세요.',
|
||||
mfa: '보안을 확인하기 위해 MFA를 잠금 해제하여 유료 플랜으로 업그레이드하세요. 궁금한 점이 있으면 <a>문의하세요</a>.',
|
||||
/** UNTRANSLATED */
|
||||
organizations:
|
||||
'Unlock organizations by upgrading to a paid plan. Don’t hesitate to <a>contact us</a> if you need any assistance.',
|
||||
};
|
||||
|
||||
export default Object.freeze(paywall);
|
||||
|
|
|
@ -46,6 +46,9 @@ const paywall = {
|
|||
hooks_other:
|
||||
'Osiągnięto limit {{count, number}} webhooków w planie <planName/>. Ulepsz plan, aby tworzyć więcej webhooków. Jeśli potrzebujesz pomocy, nie wahaj się <a>skontaktować z nami</a>.',
|
||||
mfa: 'Odblokuj MFA, aby zweryfikować bezpieczeństwo, przechodząc na płatny plan. Nie wahaj się <a>skontaktować z nami</a>, jeśli potrzebujesz pomocy.',
|
||||
/** UNTRANSLATED */
|
||||
organizations:
|
||||
'Unlock organizations by upgrading to a paid plan. Don’t hesitate to <a>contact us</a> if you need any assistance.',
|
||||
};
|
||||
|
||||
export default Object.freeze(paywall);
|
||||
|
|
|
@ -46,6 +46,9 @@ const paywall = {
|
|||
hooks_other:
|
||||
'Atingiu o limite de {{count, number}} webhooks de <planName/>. Atualize o plano para criar mais webhooks. Não hesite em <a>Contacte-nos</a> se precisar de ajuda.',
|
||||
mfa: 'Desbloqueie o MFA para verificar a segurança, fazendo upgrade para um plano pago. Não hesite em <a>nos contatar</a> se precisar de alguma assistência.',
|
||||
/** UNTRANSLATED */
|
||||
organizations:
|
||||
'Unlock organizations by upgrading to a paid plan. Don’t hesitate to <a>contact us</a> if you need any assistance.',
|
||||
};
|
||||
|
||||
export default Object.freeze(paywall);
|
||||
|
|
|
@ -46,6 +46,9 @@ const paywall = {
|
|||
hooks_other:
|
||||
'Atingiu o limite de {{count, number}} webhooks de <planName/>. Atualize o plano para criar mais webhooks. Não hesite em <a>Contacte-nos</a> se precisar de ajuda.',
|
||||
mfa: 'Desbloqueie o MFA para a verificação de segurança ao atualizar para um plano pago. Não hesite em <a>entrar em contato conosco</a> se precisar de assistência.',
|
||||
/** UNTRANSLATED */
|
||||
organizations:
|
||||
'Unlock organizations by upgrading to a paid plan. Don’t hesitate to <a>contact us</a> if you need any assistance.',
|
||||
};
|
||||
|
||||
export default Object.freeze(paywall);
|
||||
|
|
|
@ -46,6 +46,9 @@ const paywall = {
|
|||
hooks_other:
|
||||
'Достигнут лимит {{count, number}} вебхуков в плане <planName/>. Повысьте план, чтобы создать больше вебхуков. Если вам нужна помощь, не стесняйтесь <a>связаться с нами</a>.',
|
||||
mfa: 'Разблокируйте MFA для повышения безопасности с помощью перехода на платный план. Не стесняйтесь <a>связаться с нами</a>, если вам нужна помощь.',
|
||||
/** UNTRANSLATED */
|
||||
organizations:
|
||||
'Unlock organizations by upgrading to a paid plan. Don’t hesitate to <a>contact us</a> if you need any assistance.',
|
||||
};
|
||||
|
||||
export default Object.freeze(paywall);
|
||||
|
|
|
@ -46,6 +46,9 @@ const paywall = {
|
|||
hooks_other:
|
||||
'{{count, number}} <planName/> webhook sınırına ulaşıldı. Daha fazla webhook oluşturmak için planı yükseltin. Yardıma ihtiyacınız olursa, <a>iletişime geçin</a>.',
|
||||
mfa: "Güvenliği kontrol etmek için MFA'yı bir ücretli plana geçerek kilidini açın. Yardıma ihtiyacınız olursa bize <a>iletişim kurmaktan</a> çekinmeyin.",
|
||||
/** UNTRANSLATED */
|
||||
organizations:
|
||||
'Unlock organizations by upgrading to a paid plan. Don’t hesitate to <a>contact us</a> if you need any assistance.',
|
||||
};
|
||||
|
||||
export default Object.freeze(paywall);
|
||||
|
|
|
@ -46,6 +46,9 @@ const paywall = {
|
|||
hooks_other:
|
||||
'已达到<planName/>的{{count, number}}个 Webhook 限制。升级计划以创建更多 Webhook。如需任何帮助,请<a>联系我们</a>。',
|
||||
mfa: '升级到付费计划以解锁MFA进行安全验证。如果需要任何帮助,请随时<a>联系我们</a>。',
|
||||
/** UNTRANSLATED */
|
||||
organizations:
|
||||
'Unlock organizations by upgrading to a paid plan. Don’t hesitate to <a>contact us</a> if you need any assistance.',
|
||||
};
|
||||
|
||||
export default Object.freeze(paywall);
|
||||
|
|
|
@ -46,6 +46,9 @@ const paywall = {
|
|||
hooks_other:
|
||||
'已達到<planName/>的{{count, number}}個 Webhook 限制。升級計劃以創建更多 Webhook。如需任何幫助,請<a>聯繫我們</a>。',
|
||||
mfa: '升級到付費計劃以解鎖MFA以提高安全性。如果需要任何協助,請隨時<a>聯繫我們</a>。',
|
||||
/** UNTRANSLATED */
|
||||
organizations:
|
||||
'Unlock organizations by upgrading to a paid plan. Don’t hesitate to <a>contact us</a> if you need any assistance.',
|
||||
};
|
||||
|
||||
export default Object.freeze(paywall);
|
||||
|
|
|
@ -46,6 +46,9 @@ const paywall = {
|
|||
hooks_other:
|
||||
'已達到<planName/>的{{count, number}}個 Webhook 限制。升級計劃以創建更多 Webhook。如需任何幫助,請<a>聯繫我們</a>。',
|
||||
mfa: '升級到付費計劃以解鎖MFA以提高安全性。如果需要任何協助,請隨時<a>聯繫我們</a>。',
|
||||
/** UNTRANSLATED */
|
||||
organizations:
|
||||
'Unlock organizations by upgrading to a paid plan. Don’t hesitate to <a>contact us</a> if you need any assistance.',
|
||||
};
|
||||
|
||||
export default Object.freeze(paywall);
|
||||
|
|
Loading…
Add table
Reference in a new issue