mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
feat: add add-on feature notice/tag
This commit is contained in:
parent
a731b09122
commit
e8ed7178c1
46 changed files with 894 additions and 48 deletions
packages
console/src
components
AddOnNoticeFooter
ApplicationCreation/CreateForm
FeatureTag
ds-components/ModalLayout
hooks
pages
ApiResources/components/CreateForm
CustomizeJwt
EnterpriseSso
Mfa
OrganizationTemplate
Organizations
TenantSettings/TenantMembers
phrases/src/locales
de/translation/admin-console/upsell
en/translation/admin-console/upsell
es/translation/admin-console/upsell
fr/translation/admin-console/upsell
it/translation/admin-console/upsell
ja/translation/admin-console/upsell
ko/translation/admin-console/upsell
pl-pl/translation/admin-console/upsell
pt-br/translation/admin-console/upsell
pt-pt/translation/admin-console/upsell
ru/translation/admin-console/upsell
tr-tr/translation/admin-console/upsell
zh-cn/translation/admin-console/upsell
zh-hk/translation/admin-console/upsell
zh-tw/translation/admin-console/upsell
|
@ -0,0 +1,17 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: _.unit(6);
|
||||
padding: _.unit(6);
|
||||
background-color: var(--color-info-container);
|
||||
margin: 0 _.unit(-6) _.unit(-6);
|
||||
flex: 1; // Should display in full width
|
||||
|
||||
.description {
|
||||
flex: 1;
|
||||
flex-shrink: 0;
|
||||
font: var(--font-body-2);
|
||||
}
|
||||
}
|
30
packages/console/src/components/AddOnNoticeFooter/index.tsx
Normal file
30
packages/console/src/components/AddOnNoticeFooter/index.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { type AdminConsoleKey } from '@logto/phrases';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import Button from '@/ds-components/Button';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
readonly children: ReactNode;
|
||||
readonly isLoading?: boolean;
|
||||
readonly buttonTitle?: AdminConsoleKey;
|
||||
readonly onClick: () => void;
|
||||
};
|
||||
|
||||
function AddOnNoticeFooter({ children, isLoading, onClick, buttonTitle }: Props) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.description}>{children}</div>
|
||||
<Button
|
||||
size="large"
|
||||
type="primary"
|
||||
title={buttonTitle ?? 'upsell.upgrade_plan'}
|
||||
isLoading={isLoading}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddOnNoticeFooter;
|
|
@ -0,0 +1,3 @@
|
|||
.strong {
|
||||
font-weight: 500;
|
||||
}
|
|
@ -2,14 +2,18 @@ import { ApplicationType, ReservedPlanId } from '@logto/schemas';
|
|||
import { useContext } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import AddOnNoticeFooter from '@/components/AddOnNoticeFooter';
|
||||
import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import QuotaGuardFooter from '@/components/QuotaGuardFooter';
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
import TextLink from '@/ds-components/TextLink';
|
||||
import useApplicationsUsage from '@/hooks/use-applications-usage';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
readonly selectedType?: ApplicationType;
|
||||
readonly isLoading: boolean;
|
||||
|
@ -18,17 +22,43 @@ type Props = {
|
|||
};
|
||||
|
||||
function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props) {
|
||||
const { currentPlan, currentSku } = useContext(SubscriptionDataContext);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.upsell.paywall' });
|
||||
const { currentPlan, currentSku, logtoSkus } = useContext(SubscriptionDataContext);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.upsell' });
|
||||
const {
|
||||
hasAppsReachedLimit,
|
||||
hasMachineToMachineAppsReachedLimit,
|
||||
hasThirdPartyAppsReachedLimit,
|
||||
} = useApplicationsUsage();
|
||||
const addOnUnitPrice = logtoSkus.find(({ id }) => id === currentPlan.id)?.unitPrice ?? 0;
|
||||
|
||||
if (selectedType) {
|
||||
const { id: planId, name: planName, quota } = currentPlan;
|
||||
|
||||
if (
|
||||
selectedType === ApplicationType.MachineToMachine &&
|
||||
isDevFeaturesEnabled &&
|
||||
planId === ReservedPlanId.Pro
|
||||
) {
|
||||
return (
|
||||
<AddOnNoticeFooter
|
||||
isLoading={isLoading}
|
||||
buttonTitle="applications.create"
|
||||
onClick={onClickCreate}
|
||||
>
|
||||
<Trans
|
||||
components={{
|
||||
span: <span className={styles.strong} />,
|
||||
a: <TextLink to="https://blog.logto.io/pricing-add-ons/" />,
|
||||
}}
|
||||
>
|
||||
{t('add_on.footer.machine_to_machine_app', {
|
||||
price: Number(addOnUnitPrice) / 100,
|
||||
})}
|
||||
</Trans>
|
||||
</AddOnNoticeFooter>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
selectedType === ApplicationType.MachineToMachine &&
|
||||
hasMachineToMachineAppsReachedLimit &&
|
||||
|
@ -42,7 +72,7 @@ function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props)
|
|||
a: <ContactUsPhraseLink />,
|
||||
}}
|
||||
>
|
||||
{t('machine_to_machine_feature')}
|
||||
{t('paywall.machine_to_machine_feature')}
|
||||
</Trans>
|
||||
</QuotaGuardFooter>
|
||||
);
|
||||
|
@ -57,7 +87,7 @@ function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props)
|
|||
a: <ContactUsPhraseLink />,
|
||||
}}
|
||||
>
|
||||
{t('third_party_apps')}
|
||||
{t('paywall.third_party_apps')}
|
||||
</Trans>
|
||||
</QuotaGuardFooter>
|
||||
);
|
||||
|
@ -72,7 +102,7 @@ function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props)
|
|||
planName: <PlanName skuId={currentSku.id} name={planName} />,
|
||||
}}
|
||||
>
|
||||
{t('applications', { count: quota.applicationsLimit ?? 0 })}
|
||||
{t('paywall.applications', { count: quota.applicationsLimit ?? 0 })}
|
||||
</Trans>
|
||||
</QuotaGuardFooter>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { type AdminConsoleKey } from '@logto/phrases';
|
||||
import type { Application } from '@logto/schemas';
|
||||
import { ApplicationType } from '@logto/schemas';
|
||||
import { type ReactElement, useMemo } from 'react';
|
||||
import { ApplicationType, ReservedPlanId } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { type ReactElement, useContext, useMemo } from 'react';
|
||||
import { useController, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -9,6 +10,8 @@ import Modal from 'react-modal';
|
|||
import { useSWRConfig } from 'swr';
|
||||
|
||||
import { GtagConversionId, reportConversion } from '@/components/Conversion/utils';
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
import ModalLayout from '@/ds-components/ModalLayout';
|
||||
|
@ -52,6 +55,9 @@ function CreateForm({
|
|||
} = useForm<FormData>({
|
||||
defaultValues: { type: defaultCreateType, isThirdParty: isDefaultCreateThirdParty },
|
||||
});
|
||||
const {
|
||||
currentSubscription: { planId },
|
||||
} = useContext(SubscriptionDataContext);
|
||||
const { user } = useCurrentUser();
|
||||
const { mutate: mutateGlobal } = useSWRConfig();
|
||||
|
||||
|
@ -115,6 +121,9 @@ function CreateForm({
|
|||
<ModalLayout
|
||||
title="applications.create"
|
||||
subtitle={subtitleElement}
|
||||
paywall={conditional(
|
||||
isDevFeaturesEnabled && planId === ReservedPlanId.Pro && ReservedPlanId.Pro
|
||||
)}
|
||||
size={defaultCreateType ? 'medium' : 'large'}
|
||||
footer={
|
||||
<Footer
|
||||
|
|
19
packages/console/src/components/FeatureTag/AddOnTag.tsx
Normal file
19
packages/console/src/components/FeatureTag/AddOnTag.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import classNames from 'classnames';
|
||||
|
||||
/**
|
||||
* AddOnTag static component
|
||||
*
|
||||
* Used to indicate that a feature is add-on feature and will be charged according to usage.
|
||||
*/
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
readonly className?: string;
|
||||
};
|
||||
|
||||
function AddOnTag({ className }: Props) {
|
||||
return <div className={classNames(styles.tag, styles.beta, className)}>Add-on</div>;
|
||||
}
|
||||
|
||||
export default AddOnTag;
|
|
@ -1,9 +1,12 @@
|
|||
import { type ReservedPlanId } from '@logto/schemas';
|
||||
import { ReservedPlanId } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
|
||||
import AddOnTag from './AddOnTag';
|
||||
import styles from './index.module.scss';
|
||||
|
||||
export { default as BetaTag } from './BetaTag';
|
||||
|
@ -50,6 +53,9 @@ export type Props = {
|
|||
function FeatureTag(props: Props) {
|
||||
const { className } = props;
|
||||
const { isDevTenant } = useContext(TenantsContext);
|
||||
const {
|
||||
currentSubscription: { planId },
|
||||
} = useContext(SubscriptionDataContext);
|
||||
|
||||
const { isVisible, plan } = props;
|
||||
|
||||
|
@ -59,6 +65,11 @@ function FeatureTag(props: Props) {
|
|||
return null;
|
||||
}
|
||||
|
||||
// Show the add-on tag for Pro plan when dev features are enabled.
|
||||
if (isDevFeaturesEnabled && planId === ReservedPlanId.Pro) {
|
||||
return <AddOnTag className={className} />;
|
||||
}
|
||||
|
||||
return <div className={classNames(styles.tag, className)}>{plan}</div>;
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ export type Props = {
|
|||
readonly className?: string;
|
||||
readonly size?: 'medium' | 'large' | 'xlarge';
|
||||
readonly headerIcon?: ReactElement;
|
||||
} & Pick<CardTitleProps, 'learnMoreLink' | 'title' | 'subtitle' | 'isWordWrapEnabled'>;
|
||||
} & Pick<CardTitleProps, 'learnMoreLink' | 'title' | 'subtitle' | 'isWordWrapEnabled' | 'paywall'>;
|
||||
|
||||
function ModalLayout({
|
||||
children,
|
||||
|
|
|
@ -19,6 +19,7 @@ const userPreferencesGuard = z.object({
|
|||
managementApiAcknowledged: z.boolean().optional(),
|
||||
roleWithManagementApiAccessNotificationAcknowledged: z.boolean().optional(),
|
||||
m2mRoleNotificationAcknowledged: z.boolean().optional(),
|
||||
mfaUpsellNoticeAcknowledged: z.boolean().optional(),
|
||||
});
|
||||
|
||||
type UserPreferences = z.infer<typeof userPreferencesGuard>;
|
||||
|
|
|
@ -2,14 +2,18 @@ import { ReservedPlanId } from '@logto/schemas';
|
|||
import { useContext } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import AddOnNoticeFooter from '@/components/AddOnNoticeFooter';
|
||||
import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
|
||||
import PlanName from '@/components/PlanName';
|
||||
import QuotaGuardFooter from '@/components/QuotaGuardFooter';
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
import TextLink from '@/ds-components/TextLink';
|
||||
import useApiResourcesUsage from '@/hooks/use-api-resources-usage';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
readonly isCreationLoading: boolean;
|
||||
readonly onClickCreate: () => void;
|
||||
|
@ -19,10 +23,13 @@ function Footer({ isCreationLoading, onClickCreate }: Props) {
|
|||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const {
|
||||
currentPlan,
|
||||
logtoSkus,
|
||||
currentSubscription: { planId },
|
||||
currentSubscriptionUsage: { resourcesLimit },
|
||||
currentSku,
|
||||
} = useContext(SubscriptionDataContext);
|
||||
const { hasReachedLimit } = useApiResourcesUsage();
|
||||
const addOnUnitPrice = logtoSkus.find(({ id }) => id === planId)?.unitPrice ?? 0;
|
||||
|
||||
if (
|
||||
hasReachedLimit &&
|
||||
|
@ -47,6 +54,27 @@ function Footer({ isCreationLoading, onClickCreate }: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
if (isDevFeaturesEnabled && planId === ReservedPlanId.Pro) {
|
||||
return (
|
||||
<AddOnNoticeFooter
|
||||
isLoading={isCreationLoading}
|
||||
buttonTitle="api_resources.create"
|
||||
onClick={onClickCreate}
|
||||
>
|
||||
<Trans
|
||||
components={{
|
||||
span: <span className={styles.strong} />,
|
||||
a: <TextLink to="https://blog.logto.io/pricing-add-ons/" />,
|
||||
}}
|
||||
>
|
||||
{t('upsell.add_on.footer.api_resource', {
|
||||
price: Number(addOnUnitPrice) / 100,
|
||||
})}
|
||||
</Trans>
|
||||
</AddOnNoticeFooter>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
isLoading={isCreationLoading}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.strong {
|
||||
font-weight: 500;
|
||||
}
|
|
@ -1,10 +1,14 @@
|
|||
import { isValidUrl } from '@logto/core-kit';
|
||||
import { type Resource } from '@logto/schemas';
|
||||
import { ReservedPlanId, type Resource } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { useContext } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import Modal from 'react-modal';
|
||||
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
import ModalLayout from '@/ds-components/ModalLayout';
|
||||
import TextInput from '@/ds-components/TextInput';
|
||||
|
@ -26,6 +30,9 @@ type Props = {
|
|||
|
||||
function CreateForm({ onClose }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const {
|
||||
currentSubscription: { planId },
|
||||
} = useContext(SubscriptionDataContext);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
|
@ -60,6 +67,9 @@ function CreateForm({ onClose }: Props) {
|
|||
<ModalLayout
|
||||
title="api_resources.create"
|
||||
subtitle="api_resources.subtitle"
|
||||
paywall={conditional(
|
||||
isDevFeaturesEnabled && planId === ReservedPlanId.Pro && ReservedPlanId.Pro
|
||||
)}
|
||||
footer={<Footer isCreationLoading={isSubmitting} onClickCreate={onSubmit} />}
|
||||
onClose={onClose}
|
||||
>
|
||||
|
|
|
@ -11,10 +11,11 @@ import useTenantPathname from '@/hooks/use-tenant-pathname';
|
|||
import { getPagePath } from '@/pages/CustomizeJwt/utils/path';
|
||||
|
||||
type Props = {
|
||||
readonly isDisabled: boolean;
|
||||
readonly tokenType: LogtoJwtTokenKeyType;
|
||||
};
|
||||
|
||||
function CreateButton({ tokenType }: Props) {
|
||||
function CreateButton({ isDisabled, tokenType }: Props) {
|
||||
const link = getPagePath(tokenType, 'create');
|
||||
const { navigate } = useTenantPathname();
|
||||
const { show } = useConfirmModal();
|
||||
|
@ -58,6 +59,7 @@ function CreateButton({ tokenType }: Props) {
|
|||
<Button
|
||||
type="primary"
|
||||
title="jwt_claims.custom_jwt_create_button"
|
||||
disabled={isDevFeaturesEnabled && isDisabled}
|
||||
onClick={onCreateButtonClick}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.inlineNotification {
|
||||
padding: _.unit(3) _.unit(4);
|
||||
font: var(--font-body-2);
|
||||
display: flex;
|
||||
border-radius: 8px;
|
||||
align-items: center;
|
||||
gap: _.unit(3);
|
||||
|
||||
&.shadow {
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-1);
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
// Cancel the margin from MDX generated paragraphs
|
||||
&:has(> p) {
|
||||
margin: _.unit(-4) 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.info {
|
||||
background: var(--color-info-container);
|
||||
|
||||
.icon {
|
||||
color: var(--color-on-info-container);
|
||||
}
|
||||
}
|
||||
|
||||
&.alert {
|
||||
background: var(--color-alert-container);
|
||||
}
|
||||
|
||||
&.success {
|
||||
background: var(--color-success-container);
|
||||
}
|
||||
|
||||
&.error {
|
||||
background: var(--color-error-container);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import classNames from 'classnames';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
|
||||
import Button from '@/ds-components/Button';
|
||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
readonly isVisible: boolean;
|
||||
readonly className?: string;
|
||||
};
|
||||
|
||||
function UpsellNotice({ isVisible, className }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { navigate } = useTenantPathname();
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.inlineNotification, styles.info, styles.plain, className)}>
|
||||
<div className={styles.content}>
|
||||
<Trans
|
||||
components={{
|
||||
a: <ContactUsPhraseLink />,
|
||||
}}
|
||||
>
|
||||
{t('upsell.paywall.custom_jwt.description')}
|
||||
</Trans>
|
||||
</div>
|
||||
<div className={styles.action}>
|
||||
<Button
|
||||
title="upsell.upgrade_plan"
|
||||
type="primary"
|
||||
size="medium"
|
||||
onClick={() => {
|
||||
navigate('/tenant-settings/subscription');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UpsellNotice;
|
|
@ -10,6 +10,10 @@
|
|||
margin-bottom: _.unit(4);
|
||||
}
|
||||
|
||||
.inlineNotice {
|
||||
margin-bottom: _.unit(4);
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useCallback, useContext, useState } from 'react';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import FormCard, { FormCardSkeleton } from '@/components/FormCard';
|
||||
import { isCloud } from '@/consts/env';
|
||||
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import CardTitle from '@/ds-components/CardTitle';
|
||||
|
@ -13,6 +13,7 @@ import FormField from '@/ds-components/FormField';
|
|||
import CreateButton from './CreateButton';
|
||||
import CustomizerItem from './CustomizerItem';
|
||||
import DeleteConfirmModal from './DeleteConfirmModal';
|
||||
import UpsellNotice from './UpsellNotice';
|
||||
import styles from './index.module.scss';
|
||||
import useJwtCustomizer from './use-jwt-customizer';
|
||||
|
||||
|
@ -20,8 +21,15 @@ function CustomizeJwt() {
|
|||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
const { isDevTenant } = useContext(TenantsContext);
|
||||
const { currentPlan } = useContext(SubscriptionDataContext);
|
||||
const isCustomJwtEnabled = !isCloud || currentPlan.quota.customJwtEnabled;
|
||||
const {
|
||||
currentPlan,
|
||||
currentSubscription: { planId },
|
||||
currentSubscriptionQuota: { customJwtEnabled },
|
||||
} = useContext(SubscriptionDataContext);
|
||||
const isCustomJwtEnabled =
|
||||
!isCloud || (isDevFeaturesEnabled ? customJwtEnabled : currentPlan.quota.customJwtEnabled);
|
||||
|
||||
const showPaywall = planId === ReservedPlanId.Free;
|
||||
|
||||
const [deleteModalTokenType, setDeleteModalTokenType] = useState<LogtoJwtTokenKeyType>();
|
||||
|
||||
|
@ -40,6 +48,9 @@ function CustomizeJwt() {
|
|||
subtitle="jwt_claims.description"
|
||||
className={styles.header}
|
||||
/>
|
||||
{isDevFeaturesEnabled && (
|
||||
<UpsellNotice isVisible={showPaywall} className={styles.inlineNotice} />
|
||||
)}
|
||||
<div className={styles.container}>
|
||||
{isLoading && (
|
||||
<>
|
||||
|
@ -60,7 +71,10 @@ function CustomizeJwt() {
|
|||
onDelete={onDeleteHandler}
|
||||
/>
|
||||
) : (
|
||||
<CreateButton tokenType={LogtoJwtTokenKeyType.AccessToken} />
|
||||
<CreateButton
|
||||
isDisabled={showPaywall}
|
||||
tokenType={LogtoJwtTokenKeyType.AccessToken}
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
</FormCard>
|
||||
|
@ -75,7 +89,10 @@ function CustomizeJwt() {
|
|||
onDelete={onDeleteHandler}
|
||||
/>
|
||||
) : (
|
||||
<CreateButton tokenType={LogtoJwtTokenKeyType.ClientCredentials} />
|
||||
<CreateButton
|
||||
isDisabled={showPaywall}
|
||||
tokenType={LogtoJwtTokenKeyType.ClientCredentials}
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
</FormCard>
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.strong {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.textDivider {
|
||||
font: var(--font-body-2);
|
||||
color: var(--color-text-secondary);
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import {
|
||||
ReservedPlanId,
|
||||
type RequestErrorBody,
|
||||
type SsoConnectorProvidersResponse,
|
||||
type SsoConnectorWithProviderConfig,
|
||||
} from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { HTTPError } from 'ky';
|
||||
import { useContext, useMemo, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
@ -10,6 +12,7 @@ import { Trans, useTranslation } from 'react-i18next';
|
|||
import Modal from 'react-modal';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import AddOnNoticeFooter from '@/components/AddOnNoticeFooter';
|
||||
import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
|
||||
import Skeleton from '@/components/CreateConnectorForm/Skeleton';
|
||||
import { getConnectorRadioGroupSize } from '@/components/CreateConnectorForm/utils';
|
||||
|
@ -21,6 +24,7 @@ import DynamicT from '@/ds-components/DynamicT';
|
|||
import FormField from '@/ds-components/FormField';
|
||||
import ModalLayout from '@/ds-components/ModalLayout';
|
||||
import TextInput from '@/ds-components/TextInput';
|
||||
import TextLink from '@/ds-components/TextLink';
|
||||
import useApi, { type RequestError } from '@/hooks/use-api';
|
||||
import modalStyles from '@/scss/modal.module.scss';
|
||||
import { trySubmitSafe } from '@/utils/form';
|
||||
|
@ -42,7 +46,12 @@ const duplicateConnectorNameErrorCode = 'single_sign_on.duplicate_connector_name
|
|||
|
||||
function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { currentPlan, currentSubscriptionQuota } = useContext(SubscriptionDataContext);
|
||||
const {
|
||||
currentPlan,
|
||||
logtoSkus,
|
||||
currentSubscription: { planId },
|
||||
currentSubscriptionQuota,
|
||||
} = useContext(SubscriptionDataContext);
|
||||
const [selectedProviderName, setSelectedProviderName] = useState<string>();
|
||||
|
||||
const isSsoEnabled =
|
||||
|
@ -51,6 +60,7 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) {
|
|||
? currentSubscriptionQuota.enterpriseSsoLimit === null ||
|
||||
currentSubscriptionQuota.enterpriseSsoLimit > 0
|
||||
: currentPlan.quota.ssoEnabled);
|
||||
const addOnUnitPrice = logtoSkus.find(({ id }) => id === planId)?.unitPrice ?? 0;
|
||||
|
||||
const { data, error } = useSWR<SsoConnectorProvidersResponse, RequestError>(
|
||||
'api/sso-connector-providers'
|
||||
|
@ -133,8 +143,31 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) {
|
|||
>
|
||||
<ModalLayout
|
||||
title="enterprise_sso.create_modal.title"
|
||||
paywall={conditional(
|
||||
isDevFeaturesEnabled && planId === ReservedPlanId.Pro && ReservedPlanId.Pro
|
||||
)}
|
||||
footer={
|
||||
isSsoEnabled ? (
|
||||
conditional(
|
||||
isDevFeaturesEnabled && planId === ReservedPlanId.Pro && (
|
||||
<AddOnNoticeFooter
|
||||
buttonTitle="enterprise_sso.create_modal.create_button_text"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
<Trans
|
||||
components={{
|
||||
span: <span className={styles.strong} />,
|
||||
a: <TextLink to="https://blog.logto.io/pricing-add-ons/" />,
|
||||
}}
|
||||
>
|
||||
{t('upsell.add_on.footer.enterprise_sso', {
|
||||
price: Number(addOnUnitPrice) / 100,
|
||||
planName: t('subscription.pro_plan'),
|
||||
})}
|
||||
</Trans>
|
||||
</AddOnNoticeFooter>
|
||||
)
|
||||
) ??
|
||||
(isSsoEnabled ? (
|
||||
<Button
|
||||
title="enterprise_sso.create_modal.create_button_text"
|
||||
type="primary"
|
||||
|
@ -157,7 +190,7 @@ function SsoCreationModal({ isOpen, onClose: rawOnClose }: Props) {
|
|||
{t('upsell.paywall.sso_connectors')}
|
||||
</Trans>
|
||||
</QuotaGuardFooter>
|
||||
)
|
||||
))
|
||||
}
|
||||
size="xlarge"
|
||||
onClose={onClose}
|
||||
|
|
|
@ -11,7 +11,7 @@ import EnterpriseSsoConnectorEmpty from '@/assets/images/sso-connector-empty.svg
|
|||
import ItemPreview from '@/components/ItemPreview';
|
||||
import ListPage from '@/components/ListPage';
|
||||
import { defaultPageSize } from '@/consts';
|
||||
import { isCloud } from '@/consts/env';
|
||||
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import Button from '@/ds-components/Button';
|
||||
|
@ -36,13 +36,21 @@ function EnterpriseSso() {
|
|||
const { navigate } = useTenantPathname();
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { isDevTenant } = useContext(TenantsContext);
|
||||
const { currentPlan } = useContext(SubscriptionDataContext);
|
||||
const {
|
||||
currentPlan,
|
||||
currentSubscription: { planId },
|
||||
currentSubscriptionQuota,
|
||||
} = useContext(SubscriptionDataContext);
|
||||
|
||||
const [{ page }, updateSearchParameters] = useSearchParametersWatcher({
|
||||
page: 1,
|
||||
});
|
||||
|
||||
const isSsoEnabled = !isCloud || currentPlan.quota.ssoEnabled;
|
||||
const isSsoEnabled =
|
||||
!isCloud ||
|
||||
(isDevFeaturesEnabled
|
||||
? currentSubscriptionQuota.enterpriseSsoLimit !== 0
|
||||
: currentPlan.quota.ssoEnabled);
|
||||
|
||||
const url = buildUrl('api/sso-connectors', {
|
||||
page: String(page),
|
||||
|
@ -59,7 +67,9 @@ function EnterpriseSso() {
|
|||
return (
|
||||
<ListPage
|
||||
title={{
|
||||
paywall: conditional((!isSsoEnabled || isDevTenant) && ReservedPlanId.Pro),
|
||||
paywall: isDevFeaturesEnabled
|
||||
? conditional(planId === ReservedPlanId.Pro && ReservedPlanId.Pro)
|
||||
: conditional((!isSsoEnabled || isDevTenant) && ReservedPlanId.Pro),
|
||||
title: 'enterprise_sso.title',
|
||||
subtitle: 'enterprise_sso.subtitle',
|
||||
}}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import { ReservedPlanId } from '@logto/schemas';
|
||||
import { useContext } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import InlineNotification from '@/ds-components/InlineNotification';
|
||||
import TextLink from '@/ds-components/TextLink';
|
||||
import useUserPreferences from '@/hooks/use-user-preferences';
|
||||
|
||||
type Props = {
|
||||
readonly className?: string;
|
||||
};
|
||||
|
||||
function UpsellNotice({ className }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const {
|
||||
currentSubscription: { planId },
|
||||
logtoSkus,
|
||||
} = useContext(SubscriptionDataContext);
|
||||
const {
|
||||
data: { mfaUpsellNoticeAcknowledged },
|
||||
update,
|
||||
} = useUserPreferences();
|
||||
const addOnUnitPrice = logtoSkus.find(({ id }) => id === planId)?.unitPrice ?? 0;
|
||||
|
||||
if (planId !== ReservedPlanId.Pro || mfaUpsellNoticeAcknowledged) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<InlineNotification
|
||||
action="general.got_it"
|
||||
className={className}
|
||||
onClick={() => {
|
||||
void update({ mfaUpsellNoticeAcknowledged: true });
|
||||
}}
|
||||
>
|
||||
<Trans
|
||||
components={{
|
||||
a: <TextLink to="https://blog.logto.io/pricing-add-ons/" />,
|
||||
}}
|
||||
>
|
||||
{t('upsell.add_on.mfa_inline_notification', {
|
||||
price: Number(addOnUnitPrice) / 100,
|
||||
planName: String(t('subscription.pro_plan')),
|
||||
})}
|
||||
</Trans>
|
||||
</InlineNotification>
|
||||
);
|
||||
}
|
||||
|
||||
export default UpsellNotice;
|
|
@ -1,5 +1,9 @@
|
|||
@use '@/scss/underscore' as _;
|
||||
|
||||
.upsellNotice {
|
||||
margin-bottom: _.unit(4);
|
||||
}
|
||||
|
||||
.factorField {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -22,6 +22,7 @@ import { trySubmitSafe } from '@/utils/form';
|
|||
import { type MfaConfigForm, type MfaConfig } from '../types';
|
||||
|
||||
import FactorLabel from './FactorLabel';
|
||||
import UpsellNotice from './UpsellNotice';
|
||||
import { policyOptionTitleMap } from './constants';
|
||||
import styles from './index.module.scss';
|
||||
import { convertMfaFormToConfig, convertMfaConfigToForm, validateBackupCodeFactor } from './utils';
|
||||
|
@ -84,6 +85,7 @@ function MfaForm({ data, onMfaUpdated }: Props) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<UpsellNotice className={styles.upsellNotice} />
|
||||
<DetailsForm
|
||||
isDirty={isDirty}
|
||||
isSubmitting={isSubmitting}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { cond } from '@silverhand/essentials';
|
|||
import { useContext, type ReactNode } from 'react';
|
||||
|
||||
import PageMeta from '@/components/PageMeta';
|
||||
import { isCloud } from '@/consts/env';
|
||||
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import CardTitle from '@/ds-components/CardTitle';
|
||||
|
@ -16,14 +16,23 @@ type Props = {
|
|||
|
||||
function PageWrapper({ children }: Props) {
|
||||
const { isDevTenant } = useContext(TenantsContext);
|
||||
const { currentPlan } = useContext(SubscriptionDataContext);
|
||||
const isMfaEnabled = !isCloud || currentPlan.quota.mfaEnabled;
|
||||
const {
|
||||
currentPlan,
|
||||
currentSubscription: { planId },
|
||||
currentSubscriptionQuota: { mfaEnabled },
|
||||
} = useContext(SubscriptionDataContext);
|
||||
const isMfaEnabled =
|
||||
!isCloud || (isDevFeaturesEnabled ? mfaEnabled : currentPlan.quota.mfaEnabled);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<PageMeta titleKey="mfa.title" />
|
||||
<CardTitle
|
||||
paywall={cond((!isMfaEnabled || isDevTenant) && ReservedPlanId.Pro)}
|
||||
paywall={
|
||||
isDevFeaturesEnabled
|
||||
? cond(planId === ReservedPlanId.Pro && ReservedPlanId.Pro)
|
||||
: cond((!isMfaEnabled || isDevTenant) && ReservedPlanId.Pro)
|
||||
}
|
||||
title="mfa.title"
|
||||
subtitle="mfa.description"
|
||||
className={styles.cardTitle}
|
||||
|
|
|
@ -32,7 +32,11 @@ const basePathname = '/organization-template';
|
|||
function OrganizationTemplate() {
|
||||
const { getDocumentationUrl } = useDocumentationUrl();
|
||||
const [isGuideDrawerOpen, setIsGuideDrawerOpen] = useState(false);
|
||||
const { currentPlan, currentSubscriptionQuota } = useContext(SubscriptionDataContext);
|
||||
const {
|
||||
currentPlan,
|
||||
currentSubscription: { planId },
|
||||
currentSubscriptionQuota,
|
||||
} = useContext(SubscriptionDataContext);
|
||||
const { isDevTenant } = useContext(TenantsContext);
|
||||
const isOrganizationsDisabled =
|
||||
isCloud &&
|
||||
|
@ -56,7 +60,11 @@ function OrganizationTemplate() {
|
|||
href: getDocumentationUrl(organizationTemplateLink),
|
||||
targetBlank: 'noopener',
|
||||
}}
|
||||
paywall={cond((isOrganizationsDisabled || isDevTenant) && ReservedPlanId.Pro)}
|
||||
paywall={
|
||||
isDevFeaturesEnabled
|
||||
? cond(planId === ReservedPlanId.Pro && ReservedPlanId.Pro)
|
||||
: cond((isOrganizationsDisabled || isDevTenant) && ReservedPlanId.Pro)
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
title="application_details.check_guide"
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.strong {
|
||||
font-weight: 500;
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
import { type Organization, type CreateOrganization } from '@logto/schemas';
|
||||
import { type Organization, type CreateOrganization, ReservedPlanId } from '@logto/schemas';
|
||||
import { cond, conditional } from '@silverhand/essentials';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import ReactModal from 'react-modal';
|
||||
|
||||
import AddOnNoticeFooter from '@/components/AddOnNoticeFooter';
|
||||
import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
|
||||
import QuotaGuardFooter from '@/components/QuotaGuardFooter';
|
||||
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
|
||||
|
@ -12,10 +14,13 @@ 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 TextLink from '@/ds-components/TextLink';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import modalStyles from '@/scss/modal.module.scss';
|
||||
import { trySubmitSafe } from '@/utils/form';
|
||||
|
||||
import styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
readonly isOpen: boolean;
|
||||
readonly onClose: (createdId?: string) => void;
|
||||
|
@ -24,12 +29,18 @@ type Props = {
|
|||
function CreateOrganizationModal({ isOpen, onClose }: Props) {
|
||||
const api = useApi();
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { currentPlan, currentSubscriptionQuota } = useContext(SubscriptionDataContext);
|
||||
const {
|
||||
currentPlan,
|
||||
currentSubscription: { planId },
|
||||
logtoSkus,
|
||||
currentSubscriptionQuota,
|
||||
} = useContext(SubscriptionDataContext);
|
||||
const isOrganizationsDisabled =
|
||||
isCloud &&
|
||||
!(isDevFeaturesEnabled
|
||||
? currentSubscriptionQuota.organizationsEnabled
|
||||
: currentPlan.quota.organizationsEnabled);
|
||||
const addOnUnitPrice = logtoSkus.find(({ id }) => id === planId)?.unitPrice ?? 0;
|
||||
|
||||
const {
|
||||
reset,
|
||||
|
@ -66,8 +77,32 @@ function CreateOrganizationModal({ isOpen, onClose }: Props) {
|
|||
>
|
||||
<ModalLayout
|
||||
title="organizations.create_organization"
|
||||
paywall={conditional(
|
||||
isDevFeaturesEnabled && planId === ReservedPlanId.Pro && ReservedPlanId.Pro
|
||||
)}
|
||||
footer={
|
||||
isOrganizationsDisabled ? (
|
||||
cond(
|
||||
isDevFeaturesEnabled && planId === ReservedPlanId.Pro && (
|
||||
<AddOnNoticeFooter
|
||||
isLoading={isSubmitting}
|
||||
buttonTitle="general.create"
|
||||
onClick={submit}
|
||||
>
|
||||
<Trans
|
||||
components={{
|
||||
span: <span className={styles.strong} />,
|
||||
a: <TextLink to="https://blog.logto.io/pricing-add-ons/" />,
|
||||
}}
|
||||
>
|
||||
{t('upsell.add_on.footer.organization', {
|
||||
price: Number(addOnUnitPrice) / 100,
|
||||
planName: t('subscription.pro_plan'),
|
||||
})}
|
||||
</Trans>
|
||||
</AddOnNoticeFooter>
|
||||
)
|
||||
) ??
|
||||
(isOrganizationsDisabled ? (
|
||||
<QuotaGuardFooter>
|
||||
<Trans
|
||||
components={{
|
||||
|
@ -84,7 +119,7 @@ function CreateOrganizationModal({ isOpen, onClose }: Props) {
|
|||
isLoading={isSubmitting}
|
||||
onClick={submit}
|
||||
/>
|
||||
)
|
||||
))
|
||||
}
|
||||
onClose={onClose}
|
||||
>
|
||||
|
|
|
@ -25,7 +25,11 @@ const organizationsPathname = '/organizations';
|
|||
|
||||
function Organizations() {
|
||||
const { getDocumentationUrl } = useDocumentationUrl();
|
||||
const { currentPlan, currentSubscriptionQuota } = useContext(SubscriptionDataContext);
|
||||
const {
|
||||
currentPlan,
|
||||
currentSubscription: { planId },
|
||||
currentSubscriptionQuota,
|
||||
} = useContext(SubscriptionDataContext);
|
||||
const { isDevTenant } = useContext(TenantsContext);
|
||||
|
||||
const { navigate } = useTenantPathname();
|
||||
|
@ -60,7 +64,11 @@ function Organizations() {
|
|||
<PageMeta titleKey="organizations.page_title" />
|
||||
<div className={pageLayout.headline}>
|
||||
<CardTitle
|
||||
paywall={cond((isOrganizationsDisabled || isDevTenant) && ReservedPlanId.Pro)}
|
||||
paywall={
|
||||
isDevFeaturesEnabled
|
||||
? cond(planId === ReservedPlanId.Pro && ReservedPlanId.Pro)
|
||||
: cond((isOrganizationsDisabled || isDevTenant) && ReservedPlanId.Pro)
|
||||
}
|
||||
title="organizations.title"
|
||||
subtitle="organizations.subtitle"
|
||||
learnMoreLink={{
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { TenantRole } from '@logto/schemas';
|
||||
import { ReservedPlanId, TenantRole } from '@logto/schemas';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
|
@ -6,10 +7,14 @@ import { Trans, useTranslation } from 'react-i18next';
|
|||
import ReactModal from 'react-modal';
|
||||
|
||||
import { useAuthedCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
import AddOnNoticeFooter from '@/components/AddOnNoticeFooter';
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
import ModalLayout from '@/ds-components/ModalLayout';
|
||||
import Select, { type Option } from '@/ds-components/Select';
|
||||
import TextLink from '@/ds-components/TextLink';
|
||||
import { useConfirmModal } from '@/hooks/use-confirm-modal';
|
||||
import modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
|
@ -26,13 +31,18 @@ type Props = {
|
|||
};
|
||||
|
||||
function InviteMemberModal({ isOpen, onClose }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.tenant_members' });
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { currentTenantId } = useContext(TenantsContext);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const cloudApi = useAuthedCloudApi();
|
||||
const { parseEmailOptions } = useEmailInputUtils();
|
||||
const { show } = useConfirmModal();
|
||||
const {
|
||||
currentSubscription: { planId },
|
||||
logtoSkus,
|
||||
} = useContext(SubscriptionDataContext);
|
||||
const addOnUnitPrice = logtoSkus.find(({ id }) => id === planId)?.unitPrice ?? 0;
|
||||
|
||||
const formMethods = useForm<InviteMemberForm>({
|
||||
defaultValues: {
|
||||
|
@ -57,8 +67,8 @@ function InviteMemberModal({ isOpen, onClose }: Props) {
|
|||
|
||||
const roleOptions: Array<Option<TenantRole>> = useMemo(
|
||||
() => [
|
||||
{ value: TenantRole.Admin, title: t('admin') },
|
||||
{ value: TenantRole.Collaborator, title: t('collaborator') },
|
||||
{ value: TenantRole.Admin, title: t('tenant_members.admin') },
|
||||
{ value: TenantRole.Collaborator, title: t('tenant_members.collaborator') },
|
||||
],
|
||||
[t]
|
||||
);
|
||||
|
@ -68,7 +78,7 @@ function InviteMemberModal({ isOpen, onClose }: Props) {
|
|||
const [result] = await show({
|
||||
ModalContent: () => (
|
||||
<Trans components={{ ul: <ul className={styles.list} />, li: <li /> }}>
|
||||
{t('assign_admin_confirm')}
|
||||
{t('tenant_members.assign_admin_confirm')}
|
||||
</Trans>
|
||||
),
|
||||
confirmButtonText: 'general.confirm',
|
||||
|
@ -89,7 +99,7 @@ function InviteMemberModal({ isOpen, onClose }: Props) {
|
|||
})
|
||||
)
|
||||
);
|
||||
toast.success(t('messages.invitation_sent'));
|
||||
toast.success(t('tenant_members.messages.invitation_sent'));
|
||||
onClose(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
@ -108,13 +118,37 @@ function InviteMemberModal({ isOpen, onClose }: Props) {
|
|||
<ModalLayout
|
||||
size="large"
|
||||
title="tenant_members.invite_modal.title"
|
||||
paywall={conditional(
|
||||
isDevFeaturesEnabled && planId === ReservedPlanId.Pro && ReservedPlanId.Pro
|
||||
)}
|
||||
subtitle="tenant_members.invite_modal.subtitle"
|
||||
footer={
|
||||
<Footer
|
||||
newInvitationCount={watch('emails').length}
|
||||
isLoading={isLoading}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
conditional(
|
||||
isDevFeaturesEnabled && planId === ReservedPlanId.Pro && (
|
||||
<AddOnNoticeFooter
|
||||
isLoading={isLoading}
|
||||
buttonTitle="tenant_members.invite_members"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
<Trans
|
||||
components={{
|
||||
span: <span className={styles.strong} />,
|
||||
a: <TextLink to="https://blog.logto.io/pricing-add-ons/" />,
|
||||
}}
|
||||
>
|
||||
{t('upsell.add_on.footer.tenant_members', {
|
||||
price: Number(addOnUnitPrice) / 100,
|
||||
})}
|
||||
</Trans>
|
||||
</AddOnNoticeFooter>
|
||||
)
|
||||
) ?? (
|
||||
<Footer
|
||||
newInvitationCount={watch('emails').length}
|
||||
isLoading={isLoading}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onClose={onClose}
|
||||
>
|
||||
|
@ -126,7 +160,7 @@ function InviteMemberModal({ isOpen, onClose }: Props) {
|
|||
rules={{
|
||||
validate: (value): string | true => {
|
||||
if (value.length === 0) {
|
||||
return t('errors.email_required');
|
||||
return t('tenant_members.errors.email_required');
|
||||
}
|
||||
const { errorMessage } = parseEmailOptions(value);
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
|
@ -137,7 +171,7 @@ function InviteMemberModal({ isOpen, onClose }: Props) {
|
|||
<InviteEmailsInput
|
||||
values={value}
|
||||
error={errors.emails?.message}
|
||||
placeholder={t('invite_modal.email_input_placeholder')}
|
||||
placeholder={t('tenant_members.invite_modal.email_input_placeholder')}
|
||||
parseEmailOptions={parseEmailOptions}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
|
|
@ -62,3 +62,7 @@
|
|||
padding-inline-start: _.unit(1);
|
||||
}
|
||||
}
|
||||
|
||||
.strong {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
const add_on = {
|
||||
/** UNTRANSLATED */
|
||||
mfa_inline_notification:
|
||||
'MFA is a ${{price, number}} per mo add-on for the {{planName}} plan. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
footer: {
|
||||
/** UNTRANSLATED */
|
||||
api_resource:
|
||||
'Additional resources cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
machine_to_machine_app:
|
||||
'Additional machine-to-machine apps cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
enterprise_sso:
|
||||
'Enterprise SSO cost <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
tenant_members:
|
||||
'Additional members cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
organization:
|
||||
'Organization is a <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(add_on);
|
|
@ -0,0 +1,18 @@
|
|||
const add_on = {
|
||||
mfa_inline_notification:
|
||||
'MFA is a ${{price, number}} per mo add-on for the {{planName}} plan. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
footer: {
|
||||
api_resource:
|
||||
'Additional resources cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
machine_to_machine_app:
|
||||
'Additional machine-to-machine apps cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
enterprise_sso:
|
||||
'Enterprise SSO cost <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
tenant_members:
|
||||
'Additional members cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
organization:
|
||||
'Organization is a <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(add_on);
|
|
@ -1,3 +1,4 @@
|
|||
import add_on from './add-on.js';
|
||||
import featured_plan_content from './featured-plan-content.js';
|
||||
import paywall from './paywall.js';
|
||||
|
||||
|
@ -41,6 +42,7 @@ const upsell = {
|
|||
'You have surpassed your {{item}} quota limit. Logto will add charges for the usage beyond your quota limit. Charging will commence on the day the new add-on pricing design is released. <a>Learn more</a>',
|
||||
paywall,
|
||||
featured_plan_content,
|
||||
add_on,
|
||||
};
|
||||
|
||||
export default Object.freeze(upsell);
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
const add_on = {
|
||||
/** UNTRANSLATED */
|
||||
mfa_inline_notification:
|
||||
'MFA is a ${{price, number}} per mo add-on for the {{planName}} plan. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
footer: {
|
||||
/** UNTRANSLATED */
|
||||
api_resource:
|
||||
'Additional resources cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
machine_to_machine_app:
|
||||
'Additional machine-to-machine apps cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
enterprise_sso:
|
||||
'Enterprise SSO cost <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
tenant_members:
|
||||
'Additional members cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
organization:
|
||||
'Organization is a <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(add_on);
|
|
@ -0,0 +1,24 @@
|
|||
const add_on = {
|
||||
/** UNTRANSLATED */
|
||||
mfa_inline_notification:
|
||||
'MFA is a ${{price, number}} per mo add-on for the {{planName}} plan. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
footer: {
|
||||
/** UNTRANSLATED */
|
||||
api_resource:
|
||||
'Additional resources cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
machine_to_machine_app:
|
||||
'Additional machine-to-machine apps cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
enterprise_sso:
|
||||
'Enterprise SSO cost <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
tenant_members:
|
||||
'Additional members cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
organization:
|
||||
'Organization is a <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(add_on);
|
|
@ -0,0 +1,24 @@
|
|||
const add_on = {
|
||||
/** UNTRANSLATED */
|
||||
mfa_inline_notification:
|
||||
'MFA is a ${{price, number}} per mo add-on for the {{planName}} plan. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
footer: {
|
||||
/** UNTRANSLATED */
|
||||
api_resource:
|
||||
'Additional resources cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
machine_to_machine_app:
|
||||
'Additional machine-to-machine apps cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
enterprise_sso:
|
||||
'Enterprise SSO cost <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
tenant_members:
|
||||
'Additional members cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
organization:
|
||||
'Organization is a <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(add_on);
|
|
@ -0,0 +1,24 @@
|
|||
const add_on = {
|
||||
/** UNTRANSLATED */
|
||||
mfa_inline_notification:
|
||||
'MFA is a ${{price, number}} per mo add-on for the {{planName}} plan. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
footer: {
|
||||
/** UNTRANSLATED */
|
||||
api_resource:
|
||||
'Additional resources cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
machine_to_machine_app:
|
||||
'Additional machine-to-machine apps cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
enterprise_sso:
|
||||
'Enterprise SSO cost <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
tenant_members:
|
||||
'Additional members cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
organization:
|
||||
'Organization is a <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(add_on);
|
|
@ -0,0 +1,24 @@
|
|||
const add_on = {
|
||||
/** UNTRANSLATED */
|
||||
mfa_inline_notification:
|
||||
'MFA is a ${{price, number}} per mo add-on for the {{planName}} plan. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
footer: {
|
||||
/** UNTRANSLATED */
|
||||
api_resource:
|
||||
'Additional resources cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
machine_to_machine_app:
|
||||
'Additional machine-to-machine apps cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
enterprise_sso:
|
||||
'Enterprise SSO cost <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
tenant_members:
|
||||
'Additional members cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
organization:
|
||||
'Organization is a <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(add_on);
|
|
@ -0,0 +1,24 @@
|
|||
const add_on = {
|
||||
/** UNTRANSLATED */
|
||||
mfa_inline_notification:
|
||||
'MFA is a ${{price, number}} per mo add-on for the {{planName}} plan. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
footer: {
|
||||
/** UNTRANSLATED */
|
||||
api_resource:
|
||||
'Additional resources cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
machine_to_machine_app:
|
||||
'Additional machine-to-machine apps cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
enterprise_sso:
|
||||
'Enterprise SSO cost <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
tenant_members:
|
||||
'Additional members cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
organization:
|
||||
'Organization is a <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(add_on);
|
|
@ -0,0 +1,24 @@
|
|||
const add_on = {
|
||||
/** UNTRANSLATED */
|
||||
mfa_inline_notification:
|
||||
'MFA is a ${{price, number}} per mo add-on for the {{planName}} plan. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
footer: {
|
||||
/** UNTRANSLATED */
|
||||
api_resource:
|
||||
'Additional resources cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
machine_to_machine_app:
|
||||
'Additional machine-to-machine apps cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
enterprise_sso:
|
||||
'Enterprise SSO cost <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
tenant_members:
|
||||
'Additional members cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
organization:
|
||||
'Organization is a <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(add_on);
|
|
@ -0,0 +1,24 @@
|
|||
const add_on = {
|
||||
/** UNTRANSLATED */
|
||||
mfa_inline_notification:
|
||||
'MFA is a ${{price, number}} per mo add-on for the {{planName}} plan. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
footer: {
|
||||
/** UNTRANSLATED */
|
||||
api_resource:
|
||||
'Additional resources cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
machine_to_machine_app:
|
||||
'Additional machine-to-machine apps cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
enterprise_sso:
|
||||
'Enterprise SSO cost <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
tenant_members:
|
||||
'Additional members cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
organization:
|
||||
'Organization is a <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(add_on);
|
|
@ -0,0 +1,24 @@
|
|||
const add_on = {
|
||||
/** UNTRANSLATED */
|
||||
mfa_inline_notification:
|
||||
'MFA is a ${{price, number}} per mo add-on for the {{planName}} plan. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
footer: {
|
||||
/** UNTRANSLATED */
|
||||
api_resource:
|
||||
'Additional resources cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
machine_to_machine_app:
|
||||
'Additional machine-to-machine apps cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
enterprise_sso:
|
||||
'Enterprise SSO cost <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
tenant_members:
|
||||
'Additional members cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
organization:
|
||||
'Organization is a <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(add_on);
|
|
@ -0,0 +1,24 @@
|
|||
const add_on = {
|
||||
/** UNTRANSLATED */
|
||||
mfa_inline_notification:
|
||||
'MFA is a ${{price, number}} per mo add-on for the {{planName}} plan. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
footer: {
|
||||
/** UNTRANSLATED */
|
||||
api_resource:
|
||||
'Additional resources cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
machine_to_machine_app:
|
||||
'Additional machine-to-machine apps cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
enterprise_sso:
|
||||
'Enterprise SSO cost <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
tenant_members:
|
||||
'Additional members cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
organization:
|
||||
'Organization is a <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(add_on);
|
|
@ -0,0 +1,24 @@
|
|||
const add_on = {
|
||||
/** UNTRANSLATED */
|
||||
mfa_inline_notification:
|
||||
'MFA is a ${{price, number}} per mo add-on for the {{planName}} plan. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
footer: {
|
||||
/** UNTRANSLATED */
|
||||
api_resource:
|
||||
'Additional resources cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
machine_to_machine_app:
|
||||
'Additional machine-to-machine apps cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
enterprise_sso:
|
||||
'Enterprise SSO cost <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
tenant_members:
|
||||
'Additional members cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
organization:
|
||||
'Organization is a <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(add_on);
|
|
@ -0,0 +1,24 @@
|
|||
const add_on = {
|
||||
/** UNTRANSLATED */
|
||||
mfa_inline_notification:
|
||||
'MFA is a ${{price, number}} per mo add-on for the {{planName}} plan. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
footer: {
|
||||
/** UNTRANSLATED */
|
||||
api_resource:
|
||||
'Additional resources cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
machine_to_machine_app:
|
||||
'Additional machine-to-machine apps cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
enterprise_sso:
|
||||
'Enterprise SSO cost <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
tenant_members:
|
||||
'Additional members cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
organization:
|
||||
'Organization is a <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(add_on);
|
|
@ -0,0 +1,24 @@
|
|||
const add_on = {
|
||||
/** UNTRANSLATED */
|
||||
mfa_inline_notification:
|
||||
'MFA is a ${{price, number}} per mo add-on for the {{planName}} plan. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
footer: {
|
||||
/** UNTRANSLATED */
|
||||
api_resource:
|
||||
'Additional resources cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
machine_to_machine_app:
|
||||
'Additional machine-to-machine apps cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
enterprise_sso:
|
||||
'Enterprise SSO cost <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
tenant_members:
|
||||
'Additional members cost <span>${{price, number}} per mo / ea</span>. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
/** UNTRANSLATED */
|
||||
organization:
|
||||
'Organization is a <span>${{price, number}} per mo / ea</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
export default Object.freeze(add_on);
|
Loading…
Add table
Reference in a new issue