mirror of
https://github.com/logto-io/logto.git
synced 2025-02-24 22:05:56 -05:00
fix: fix console issues for add-on (#6443)
* fix: fix console issues for add-on * refactor: refactor code * refactor: update * fix: fix method use case
This commit is contained in:
parent
bb98ea8301
commit
d2220f1205
29 changed files with 133 additions and 68 deletions
|
@ -37,6 +37,7 @@ function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props)
|
|||
if (
|
||||
selectedType === ApplicationType.MachineToMachine &&
|
||||
isDevFeaturesEnabled &&
|
||||
hasMachineToMachineAppsReachedLimit &&
|
||||
planId === ReservedPlanId.Pro
|
||||
) {
|
||||
return (
|
||||
|
|
|
@ -49,6 +49,7 @@ function CreateForm({
|
|||
}: Props) {
|
||||
const {
|
||||
handleSubmit,
|
||||
watch,
|
||||
control,
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
|
@ -122,7 +123,10 @@ function CreateForm({
|
|||
title="applications.create"
|
||||
subtitle={subtitleElement}
|
||||
paywall={conditional(
|
||||
isDevFeaturesEnabled && planId === ReservedPlanId.Pro && ReservedPlanId.Pro
|
||||
isDevFeaturesEnabled &&
|
||||
watch('type') === ApplicationType.MachineToMachine &&
|
||||
planId === ReservedPlanId.Pro &&
|
||||
ReservedPlanId.Pro
|
||||
)}
|
||||
size={defaultCreateType ? 'medium' : 'large'}
|
||||
footer={
|
||||
|
|
|
@ -19,10 +19,19 @@ export type Props = {
|
|||
readonly usageKey: AdminConsoleKey;
|
||||
readonly titleKey: AdminConsoleKey;
|
||||
readonly tooltipKey: AdminConsoleKey;
|
||||
readonly unitPrice: number;
|
||||
readonly className?: string;
|
||||
};
|
||||
|
||||
function ProPlanUsageCard({ usage, quota, usageKey, titleKey, tooltipKey, className }: Props) {
|
||||
function ProPlanUsageCard({
|
||||
usage,
|
||||
quota,
|
||||
unitPrice,
|
||||
usageKey,
|
||||
titleKey,
|
||||
tooltipKey,
|
||||
className,
|
||||
}: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
return (
|
||||
|
@ -38,7 +47,9 @@ function ProPlanUsageCard({ usage, quota, usageKey, titleKey, tooltipKey, classN
|
|||
a: <TextLink to="https://blog.logto.io/pricing-add-ons/" />,
|
||||
}}
|
||||
>
|
||||
{t(tooltipKey)}
|
||||
{t(tooltipKey, {
|
||||
price: unitPrice,
|
||||
})}
|
||||
</Trans>
|
||||
}
|
||||
>
|
||||
|
|
|
@ -14,7 +14,7 @@ import { formatPeriod } from '@/utils/subscription';
|
|||
|
||||
import ProPlanUsageCard, { type Props as ProPlanUsageCardProps } from './ProPlanUsageCard';
|
||||
import styles from './index.module.scss';
|
||||
import { usageKeys, usageKeyMap, titleKeyMap, tooltipKeyMap } from './utils';
|
||||
import { usageKeys, usageKeyPriceMap, usageKeyMap, titleKeyMap, tooltipKeyMap } from './utils';
|
||||
|
||||
type Props = {
|
||||
/** @deprecated */
|
||||
|
@ -67,6 +67,7 @@ function PlanUsage({ currentSubscription, currentPlan, periodicUsage: rawPeriodi
|
|||
usageKey: `subscription.usage.${usageKeyMap[key]}`,
|
||||
titleKey: `subscription.usage.${titleKeyMap[key]}`,
|
||||
tooltipKey: `subscription.usage.${tooltipKeyMap[key]}`,
|
||||
unitPrice: usageKeyPriceMap[key],
|
||||
...cond(
|
||||
key === 'tokenLimit' &&
|
||||
currentSubscriptionQuota.tokenLimit && { quota: currentSubscriptionQuota.tokenLimit }
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
import { type TFuncKey } from 'i18next';
|
||||
|
||||
import { type NewSubscriptionQuota } from '@/cloud/types/router';
|
||||
import {
|
||||
resourceAddOnUnitPrice,
|
||||
machineToMachineAddOnUnitPrice,
|
||||
tenantMembersAddOnUnitPrice,
|
||||
mfaAddOnUnitPrice,
|
||||
enterpriseSsoAddOnUnitPrice,
|
||||
organizationAddOnUnitPrice,
|
||||
tokenAddOnUnitPrice,
|
||||
hooksAddOnUnitPrice,
|
||||
} from '@/consts/subscriptions';
|
||||
|
||||
type UsageKey = Pick<
|
||||
NewSubscriptionQuota,
|
||||
|
@ -15,6 +25,7 @@ type UsageKey = Pick<
|
|||
| 'hooksLimit'
|
||||
>;
|
||||
|
||||
// We decide not to show `hooksLimit` usage in console for now.
|
||||
export const usageKeys: Array<keyof UsageKey> = [
|
||||
'mauLimit',
|
||||
'organizationsEnabled',
|
||||
|
@ -24,9 +35,20 @@ export const usageKeys: Array<keyof UsageKey> = [
|
|||
'machineToMachineLimit',
|
||||
'tenantMembersLimit',
|
||||
'tokenLimit',
|
||||
'hooksLimit',
|
||||
];
|
||||
|
||||
export const usageKeyPriceMap: Record<keyof UsageKey, number> = {
|
||||
mauLimit: 0,
|
||||
organizationsEnabled: organizationAddOnUnitPrice,
|
||||
mfaEnabled: mfaAddOnUnitPrice,
|
||||
enterpriseSsoLimit: enterpriseSsoAddOnUnitPrice,
|
||||
resourcesLimit: resourceAddOnUnitPrice,
|
||||
machineToMachineLimit: machineToMachineAddOnUnitPrice,
|
||||
tenantMembersLimit: tenantMembersAddOnUnitPrice,
|
||||
tokenLimit: tokenAddOnUnitPrice,
|
||||
hooksLimit: hooksAddOnUnitPrice,
|
||||
};
|
||||
|
||||
export const usageKeyMap: Record<
|
||||
keyof UsageKey,
|
||||
TFuncKey<'translation', 'admin_console.subscription.usage'>
|
||||
|
|
|
@ -18,6 +18,8 @@ export const tenantMembersAddOnUnitPrice = 8;
|
|||
export const mfaAddOnUnitPrice = 48;
|
||||
export const enterpriseSsoAddOnUnitPrice = 48;
|
||||
export const organizationAddOnUnitPrice = 48;
|
||||
export const tokenAddOnUnitPrice = 80;
|
||||
export const hooksAddOnUnitPrice = 2;
|
||||
/* === Add-on unit price (in USD) === */
|
||||
|
||||
/**
|
||||
|
|
|
@ -103,14 +103,15 @@ const useSubscribe = () => {
|
|||
|
||||
// Should not use hard-coded plan update here, need to update the tenant's subscription data with response from corresponding API.
|
||||
if (isDevFeaturesEnabled) {
|
||||
const { id, ...rest } = await cloudApi.get('/api/tenants/:tenantId/subscription', {
|
||||
const subscription = await cloudApi.get('/api/tenants/:tenantId/subscription', {
|
||||
params: {
|
||||
tenantId,
|
||||
},
|
||||
});
|
||||
|
||||
mutateSubscriptionQuotaAndUsages();
|
||||
onCurrentSubscriptionUpdated();
|
||||
onCurrentSubscriptionUpdated(subscription);
|
||||
const { id, ...rest } = subscription;
|
||||
|
||||
updateTenant(tenantId, {
|
||||
planId: rest.planId,
|
||||
|
|
|
@ -81,14 +81,13 @@ function CreateTenant() {
|
|||
if (collaboratorEmails.length > 0) {
|
||||
// Should not block the onboarding flow if the invitation fails.
|
||||
try {
|
||||
await Promise.all(
|
||||
collaboratorEmails.map(async (email) =>
|
||||
tenantCloudApi.post('/api/tenants/:tenantId/invitations', {
|
||||
params: { tenantId: newTenant.id },
|
||||
body: { invitee: email.value, roleName: TenantRole.Collaborator },
|
||||
})
|
||||
)
|
||||
);
|
||||
await tenantCloudApi.post('/api/tenants/:tenantId/invitations', {
|
||||
params: { tenantId: newTenant.id },
|
||||
body: {
|
||||
invitee: collaboratorEmails.map(({ value }) => value),
|
||||
roleName: TenantRole.Collaborator,
|
||||
},
|
||||
});
|
||||
toast.success(t('tenant_members.messages.invitation_sent'));
|
||||
} catch {
|
||||
toast.error(t('tenants.create_modal.invitation_failed', { duration: 5 }));
|
||||
|
|
|
@ -53,7 +53,7 @@ function Footer({ isCreationLoading, onClickCreate }: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
if (isDevFeaturesEnabled && planId === ReservedPlanId.Pro) {
|
||||
if (isDevFeaturesEnabled && hasReachedLimit && planId === ReservedPlanId.Pro) {
|
||||
return (
|
||||
<AddOnNoticeFooter
|
||||
isLoading={isCreationLoading}
|
||||
|
|
|
@ -13,6 +13,7 @@ import { GtagConversionId, reportToGoogle } from '@/components/Conversion/utils'
|
|||
import PlanName from '@/components/PlanName';
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { checkoutStateQueryKey } from '@/consts/subscriptions';
|
||||
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
|
||||
import { TenantsContext } from '@/contexts/TenantsProvider';
|
||||
import useLogtoSkus from '@/hooks/use-logto-skus';
|
||||
import useSubscriptionPlans from '@/hooks/use-subscription-plans';
|
||||
|
@ -28,6 +29,7 @@ function CheckoutSuccessCallback() {
|
|||
const { navigate } = useTenantPathname();
|
||||
const cloudApi = useCloudApi({ hideErrorToast: true });
|
||||
const { currentTenantId, navigateTenant } = useContext(TenantsContext);
|
||||
const { onCurrentSubscriptionUpdated } = useContext(SubscriptionDataContext);
|
||||
const { search } = useLocation();
|
||||
const checkoutState = new URLSearchParams(search).get(checkoutStateQueryKey);
|
||||
const { state, sessionId, callbackPage, isDowngrade } = getLocalCheckoutSession() ?? {};
|
||||
|
@ -121,6 +123,8 @@ function CheckoutSuccessCallback() {
|
|||
}
|
||||
}
|
||||
|
||||
onCurrentSubscriptionUpdated(tenantSubscription);
|
||||
|
||||
// No need to check `isDowngrade` here, since a downgrade must occur in a tenant with a Pro
|
||||
// plan, and the purchase conversion has already been reported using the same tenant ID. We
|
||||
// use the tenant ID as the transaction ID, so there's no concern about duplicate conversion
|
||||
|
@ -147,8 +151,10 @@ function CheckoutSuccessCallback() {
|
|||
logtoSkus,
|
||||
navigate,
|
||||
navigateTenant,
|
||||
onCurrentSubscriptionUpdated,
|
||||
subscriptionPlans,
|
||||
t,
|
||||
tenantSubscription,
|
||||
]);
|
||||
|
||||
if (!isValidSession && !isLoadingPlans) {
|
||||
|
|
|
@ -33,7 +33,8 @@ type Props = {
|
|||
};
|
||||
|
||||
function MfaForm({ data, onMfaUpdated }: Props) {
|
||||
const { currentPlan, currentSubscriptionQuota } = useContext(SubscriptionDataContext);
|
||||
const { currentPlan, currentSubscriptionQuota, mutateSubscriptionQuotaAndUsages } =
|
||||
useContext(SubscriptionDataContext);
|
||||
const isMfaDisabled =
|
||||
isCloud &&
|
||||
!(isDevFeaturesEnabled ? currentSubscriptionQuota.mfaEnabled : currentPlan.quota.mfaEnabled);
|
||||
|
@ -77,6 +78,7 @@ function MfaForm({ data, onMfaUpdated }: Props) {
|
|||
json: { mfa: mfaConfig },
|
||||
})
|
||||
.json<SignInExperience>();
|
||||
mutateSubscriptionQuotaAndUsages();
|
||||
reset(convertMfaConfigToForm(updatedMfaConfig));
|
||||
toast.success(t('general.saved'));
|
||||
onMfaUpdated(updatedMfaConfig);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { cond, conditional } from '@silverhand/essentials';
|
||||
import { cond } from '@silverhand/essentials';
|
||||
import { useContext, useMemo } from 'react';
|
||||
|
||||
import { type Subscription, type NewSubscriptionPeriodicUsage } from '@/cloud/types/router';
|
||||
|
@ -28,9 +28,9 @@ type Props = {
|
|||
};
|
||||
|
||||
function CurrentPlan({ subscription, subscriptionPlan, periodicUsage: rawPeriodicUsage }: Props) {
|
||||
const { currentTenant } = useContext(TenantsContext);
|
||||
const { currentSku, currentSubscription, currentSubscriptionQuota } =
|
||||
useContext(SubscriptionDataContext);
|
||||
const { currentTenant } = useContext(TenantsContext);
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
|
@ -40,7 +40,7 @@ function CurrentPlan({ subscription, subscriptionPlan, periodicUsage: rawPeriodi
|
|||
const periodicUsage = useMemo(
|
||||
() =>
|
||||
rawPeriodicUsage ??
|
||||
conditional(
|
||||
cond(
|
||||
currentTenant && {
|
||||
mauLimit: currentTenant.usage.activeUsers,
|
||||
tokenLimit: currentTenant.usage.tokenUsage,
|
||||
|
@ -50,11 +50,12 @@ function CurrentPlan({ subscription, subscriptionPlan, periodicUsage: rawPeriodi
|
|||
);
|
||||
|
||||
/**
|
||||
* After the new pricing model goes live, `upcomingInvoice` will always exist. However, for compatibility reasons, the price of the SKU's corresponding `unitPrice` will be used as a fallback when it does not exist. If `unitPrice` also does not exist, it means that the tenant does not have any applicable paid subscription, and the bill will be 0.
|
||||
* After the new pricing model goes live, `upcomingInvoice` will always exist. `upcomingInvoice` is updated more frequently than `currentSubscription.upcomingInvoice`.
|
||||
* However, for compatibility reasons, the price of the SKU's corresponding `unitPrice` will be used as a fallback when it does not exist. If `unitPrice` also does not exist, it means that the tenant does not have any applicable paid subscription, and the bill will be 0.
|
||||
*/
|
||||
const upcomingCost = useMemo(
|
||||
() => currentSubscription.upcomingInvoice?.subtotal ?? currentSku.unitPrice ?? 0,
|
||||
[currentSku.unitPrice, currentSubscription.upcomingInvoice?.subtotal]
|
||||
[currentSku.unitPrice, currentSubscription.upcomingInvoice]
|
||||
);
|
||||
|
||||
if (!periodicUsage) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext } from 'react';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { useCloudApi } from '@/cloud/hooks/use-cloud-api';
|
||||
|
@ -39,6 +39,12 @@ function Subscription() {
|
|||
})
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isCloud && isDevFeaturesEnabled) {
|
||||
onCurrentSubscriptionUpdated();
|
||||
}
|
||||
}, [onCurrentSubscriptionUpdated]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ 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';
|
||||
import { hasReachedSubscriptionQuotaLimit } from '@/utils/quota';
|
||||
|
||||
import InviteEmailsInput from '../InviteEmailsInput';
|
||||
import useEmailInputUtils from '../InviteEmailsInput/hooks';
|
||||
|
@ -41,6 +42,8 @@ function InviteMemberModal({ isOpen, onClose }: Props) {
|
|||
const { show } = useConfirmModal();
|
||||
const {
|
||||
currentSubscription: { planId },
|
||||
currentSubscriptionQuota,
|
||||
currentSubscriptionUsage: { tenantMembersLimit },
|
||||
} = useContext(SubscriptionDataContext);
|
||||
|
||||
const formMethods = useForm<InviteMemberForm>({
|
||||
|
@ -72,6 +75,12 @@ function InviteMemberModal({ isOpen, onClose }: Props) {
|
|||
[t]
|
||||
);
|
||||
|
||||
const hasTenantMembersReachedLimit = hasReachedSubscriptionQuotaLimit({
|
||||
quotaKey: 'tenantMembersLimit',
|
||||
usage: tenantMembersLimit,
|
||||
quota: currentSubscriptionQuota,
|
||||
});
|
||||
|
||||
const onSubmit = handleSubmit(async ({ emails, role }) => {
|
||||
if (role === TenantRole.Admin) {
|
||||
const [result] = await show({
|
||||
|
@ -89,19 +98,17 @@ function InviteMemberModal({ isOpen, onClose }: Props) {
|
|||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await Promise.all(
|
||||
emails.map(async (email) =>
|
||||
cloudApi.post('/api/tenants/:tenantId/invitations', {
|
||||
params: { tenantId: currentTenantId },
|
||||
body: { invitee: email.value, roleName: role },
|
||||
})
|
||||
)
|
||||
);
|
||||
toast.success(t('tenant_members.messages.invitation_sent'));
|
||||
onClose(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
if (emails.length > 0) {
|
||||
try {
|
||||
await cloudApi.post('/api/tenants/:tenantId/invitations', {
|
||||
params: { tenantId: currentTenantId },
|
||||
body: { invitee: emails.map(({ value }) => value), roleName: role },
|
||||
});
|
||||
toast.success(t('tenant_members.messages.invitation_sent'));
|
||||
onClose(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -123,24 +130,26 @@ function InviteMemberModal({ isOpen, onClose }: Props) {
|
|||
subtitle="tenant_members.invite_modal.subtitle"
|
||||
footer={
|
||||
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/" />,
|
||||
}}
|
||||
isDevFeaturesEnabled &&
|
||||
hasTenantMembersReachedLimit &&
|
||||
planId === ReservedPlanId.Pro && (
|
||||
<AddOnNoticeFooter
|
||||
isLoading={isLoading}
|
||||
buttonTitle="tenant_members.invite_members"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
{t('upsell.add_on.footer.tenant_members', {
|
||||
price: tenantMembersAddOnUnitPrice,
|
||||
})}
|
||||
</Trans>
|
||||
</AddOnNoticeFooter>
|
||||
)
|
||||
<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: tenantMembersAddOnUnitPrice,
|
||||
})}
|
||||
</Trans>
|
||||
</AddOnNoticeFooter>
|
||||
)
|
||||
) ?? (
|
||||
<Footer
|
||||
newInvitationCount={watch('emails').length}
|
||||
|
|
|
@ -17,7 +17,7 @@ const add_on = {
|
|||
'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>',
|
||||
'Organization is a <span>${{price, number}} per mo</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ const add_on = {
|
|||
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>',
|
||||
'Organization is a <span>${{price, number}} per mo</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ const add_on = {
|
|||
'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>',
|
||||
'Organization is a <span>${{price, number}} per mo</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ const add_on = {
|
|||
'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>',
|
||||
'Organization is a <span>${{price, number}} per mo</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ const add_on = {
|
|||
'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>',
|
||||
'Organization is a <span>${{price, number}} per mo</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ const add_on = {
|
|||
'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>',
|
||||
'Organization is a <span>${{price, number}} per mo</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ const add_on = {
|
|||
'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>',
|
||||
'Organization is a <span>${{price, number}} per mo</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ const add_on = {
|
|||
'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>',
|
||||
'Organization is a <span>${{price, number}} per mo</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ const add_on = {
|
|||
'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>',
|
||||
'Organization is a <span>${{price, number}} per mo</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ const add_on = {
|
|||
'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>',
|
||||
'Organization is a <span>${{price, number}} per mo</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ const add_on = {
|
|||
'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>',
|
||||
'Organization is a <span>${{price, number}} per mo</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ const add_on = {
|
|||
'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>',
|
||||
'Organization is a <span>${{price, number}} per mo</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ const add_on = {
|
|||
'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>',
|
||||
'Organization is a <span>${{price, number}} per mo</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ const add_on = {
|
|||
'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>',
|
||||
'Organization is a <span>${{price, number}} per mo</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ const add_on = {
|
|||
'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>',
|
||||
'Organization is a <span>${{price, number}} per mo</span> add-on for {{planName}}. First month prorated based on your billing cycle. <a>Learn more</a>',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue