0
Fork 0
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:
Charles Zhao 2023-11-23 12:44:42 +08:00 committed by GitHub
parent 979bf08966
commit 9a91c0ad10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 96 additions and 5 deletions

View file

@ -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}
>

View file

@ -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);

View file

@ -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();
};
}

View file

@ -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());
}

View file

@ -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());
}

View file

@ -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());
}

View file

@ -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. Dont hesitate to <a>contact us</a> if you need any assistance.',
};
export default Object.freeze(paywall);

View file

@ -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. Dont hesitate to <a>contact us</a> if you need any assistance.',
organizations:
'Unlock organizations by upgrading to a paid plan. Dont hesitate to <a>contact us</a> if you need any assistance.',
};
export default Object.freeze(paywall);

View file

@ -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. Dont hesitate to <a>contact us</a> if you need any assistance.',
};
export default Object.freeze(paywall);

View file

@ -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. Dont hesitate to <a>contact us</a> if you need any assistance.',
};
export default Object.freeze(paywall);

View file

@ -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. Dont hesitate to <a>contact us</a> if you need any assistance.',
};
export default Object.freeze(paywall);

View file

@ -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. Dont hesitate to <a>contact us</a> if you need any assistance.',
};
export default Object.freeze(paywall);

View file

@ -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. Dont hesitate to <a>contact us</a> if you need any assistance.',
};
export default Object.freeze(paywall);

View file

@ -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. Dont hesitate to <a>contact us</a> if you need any assistance.',
};
export default Object.freeze(paywall);

View file

@ -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. Dont hesitate to <a>contact us</a> if you need any assistance.',
};
export default Object.freeze(paywall);

View file

@ -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. Dont hesitate to <a>contact us</a> if you need any assistance.',
};
export default Object.freeze(paywall);

View file

@ -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. Dont hesitate to <a>contact us</a> if you need any assistance.',
};
export default Object.freeze(paywall);

View file

@ -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. Dont hesitate to <a>contact us</a> if you need any assistance.',
};
export default Object.freeze(paywall);

View file

@ -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. Dont hesitate to <a>contact us</a> if you need any assistance.',
};
export default Object.freeze(paywall);

View file

@ -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. Dont hesitate to <a>contact us</a> if you need any assistance.',
};
export default Object.freeze(paywall);

View file

@ -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. Dont hesitate to <a>contact us</a> if you need any assistance.',
};
export default Object.freeze(paywall);