mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
refactor(console): reorg downgrade plan notification modal (#5419)
This commit is contained in:
parent
be9428fa0d
commit
9fbb4196f3
31 changed files with 137 additions and 382 deletions
|
@ -1,36 +0,0 @@
|
|||
import classNames from 'classnames';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
import { type SubscriptionPlanQuota } from '@/types/subscriptions';
|
||||
|
||||
import QuotaItemPhrase from './QuotaItemPhrase';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
quotaKey: keyof SubscriptionPlanQuota;
|
||||
quotaValue: SubscriptionPlanQuota[keyof SubscriptionPlanQuota];
|
||||
icon?: ReactNode;
|
||||
suffix?: ReactNode;
|
||||
isAddOn?: boolean;
|
||||
phraseClassName?: string;
|
||||
};
|
||||
|
||||
function QuotaListItem({ icon, suffix, phraseClassName, ...rest }: Props) {
|
||||
return (
|
||||
<li className={classNames(styles.quotaListItem, icon && styles.withIcon)}>
|
||||
{/**
|
||||
* Add a `span` as a wrapper to apply the flex layout to the content.
|
||||
* If we apply the flex layout to the `li` directly, the `li` circle bullet will disappear.
|
||||
*/}
|
||||
<span className={styles.content}>
|
||||
{icon}
|
||||
<span className={phraseClassName}>
|
||||
<QuotaItemPhrase {...rest} />
|
||||
</span>
|
||||
{suffix}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default QuotaListItem;
|
|
@ -1,40 +0,0 @@
|
|||
import classNames from 'classnames';
|
||||
import { useMemo, type ReactNode } from 'react';
|
||||
|
||||
import { planQuotaItemOrder } from '@/consts/plan-quotas';
|
||||
import {
|
||||
type SubscriptionPlanQuotaEntries,
|
||||
type SubscriptionPlanQuota,
|
||||
} from '@/types/subscriptions';
|
||||
import { sortBy } from '@/utils/sort';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
entries: SubscriptionPlanQuotaEntries;
|
||||
itemRenderer: (
|
||||
quotaKey: keyof SubscriptionPlanQuota,
|
||||
quotaValue: SubscriptionPlanQuota[keyof SubscriptionPlanQuota]
|
||||
) => ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function PlanQuotaList({ entries, itemRenderer, className }: Props) {
|
||||
const sortedEntries = useMemo(
|
||||
() =>
|
||||
entries
|
||||
.slice()
|
||||
.sort(([preQuotaKey], [nextQuotaKey]) =>
|
||||
sortBy(planQuotaItemOrder)(preQuotaKey, nextQuotaKey)
|
||||
),
|
||||
[entries]
|
||||
);
|
||||
|
||||
return (
|
||||
<ul className={classNames(styles.planQuotaList, className)}>
|
||||
{sortedEntries.map(([quotaKey, quotaValue]) => itemRenderer(quotaKey, quotaValue))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlanQuotaList;
|
|
@ -1,75 +1,40 @@
|
|||
import { ReservedPlanId } from '@logto/schemas';
|
||||
|
||||
import {
|
||||
type SubscriptionPlanTableGroupKeyMap,
|
||||
SubscriptionPlanTableGroupKey,
|
||||
type SubscriptionPlanQuota,
|
||||
} from '@/types/subscriptions';
|
||||
import { type SubscriptionPlanQuota } from '@/types/subscriptions';
|
||||
|
||||
type EnabledFeatureMap = Record<string, boolean | undefined>;
|
||||
|
||||
export const communitySupportEnabledMap: EnabledFeatureMap = {
|
||||
[ReservedPlanId.Free]: true,
|
||||
[ReservedPlanId.Hobby]: true,
|
||||
[ReservedPlanId.Pro]: true,
|
||||
};
|
||||
|
||||
export const ticketSupportResponseTimeMap: Record<string, number | undefined> = {
|
||||
/**
|
||||
* Manually add this support quota item to the plan since it will be compared in the downgrade plan notification modal.
|
||||
*/
|
||||
export const ticketSupportResponseTimeMap: Record<string, number> = {
|
||||
[ReservedPlanId.Free]: 0,
|
||||
[ReservedPlanId.Hobby]: 48,
|
||||
[ReservedPlanId.Pro]: 48,
|
||||
};
|
||||
|
||||
const planTableGroupKeyMap: SubscriptionPlanTableGroupKeyMap = Object.freeze({
|
||||
[SubscriptionPlanTableGroupKey.base]: ['basePrice', 'mauLimit', 'tokenLimit'],
|
||||
[SubscriptionPlanTableGroupKey.applications]: [
|
||||
'applicationsLimit',
|
||||
'machineToMachineLimit',
|
||||
'thirdPartyApplicationsLimit',
|
||||
],
|
||||
[SubscriptionPlanTableGroupKey.resources]: ['resourcesLimit', 'scopesPerResourceLimit'],
|
||||
[SubscriptionPlanTableGroupKey.branding]: [
|
||||
'customDomainEnabled',
|
||||
'customCssEnabled',
|
||||
'appLogoAndFaviconEnabled',
|
||||
'darkModeEnabled',
|
||||
'i18nEnabled',
|
||||
],
|
||||
[SubscriptionPlanTableGroupKey.userAuthentication]: [
|
||||
'omniSignInEnabled',
|
||||
'passwordSignInEnabled',
|
||||
'passwordlessSignInEnabled',
|
||||
'emailConnectorsEnabled',
|
||||
'smsConnectorsEnabled',
|
||||
'socialConnectorsLimit',
|
||||
'mfaEnabled',
|
||||
'ssoEnabled',
|
||||
],
|
||||
[SubscriptionPlanTableGroupKey.roles]: [
|
||||
'userManagementEnabled',
|
||||
'rolesLimit',
|
||||
'machineToMachineRolesLimit',
|
||||
'scopesPerRoleLimit',
|
||||
],
|
||||
[SubscriptionPlanTableGroupKey.organizations]: [
|
||||
'organizationsEnabled',
|
||||
'allowedUsersPerOrganization',
|
||||
'invitationEnabled',
|
||||
'orgRolesLimit',
|
||||
'orgPermissionsLimit',
|
||||
'justInTimeProvisioningEnabled',
|
||||
],
|
||||
[SubscriptionPlanTableGroupKey.auditLogs]: ['auditLogsRetentionDays'],
|
||||
[SubscriptionPlanTableGroupKey.hooks]: ['hooksLimit'],
|
||||
[SubscriptionPlanTableGroupKey.support]: [
|
||||
'communitySupportEnabled',
|
||||
'ticketSupportResponseTime',
|
||||
'soc2ReportEnabled',
|
||||
'hipaaOrBaaReportEnabled',
|
||||
],
|
||||
}) satisfies SubscriptionPlanTableGroupKeyMap;
|
||||
|
||||
export const planQuotaItemOrder = Object.values(planTableGroupKeyMap).flat();
|
||||
/**
|
||||
* Define the order of quota items in the downgrade plan notification modal and not eligible for downgrade plan modal.
|
||||
*/
|
||||
export const planQuotaItemOrder: Array<keyof SubscriptionPlanQuota> = [
|
||||
'mauLimit',
|
||||
'tokenLimit',
|
||||
'applicationsLimit',
|
||||
'machineToMachineLimit',
|
||||
'thirdPartyApplicationsLimit',
|
||||
'resourcesLimit',
|
||||
'scopesPerResourceLimit',
|
||||
'customDomainEnabled',
|
||||
'omniSignInEnabled',
|
||||
'socialConnectorsLimit',
|
||||
'mfaEnabled',
|
||||
'ssoEnabled',
|
||||
'rolesLimit',
|
||||
'machineToMachineRolesLimit',
|
||||
'scopesPerRoleLimit',
|
||||
'organizationsEnabled',
|
||||
'auditLogsRetentionDays',
|
||||
'hooksLimit',
|
||||
'ticketSupportResponseTime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Unreleased quota keys will be added here, and it will effect the following:
|
||||
|
|
|
@ -22,7 +22,6 @@ export const quotaItemPhrasesMap: Record<
|
|||
scopesPerRoleLimit: 'scopes_per_role_limit.name',
|
||||
hooksLimit: 'hooks_limit.name',
|
||||
auditLogsRetentionDays: 'audit_logs_retention_days.name',
|
||||
communitySupportEnabled: 'community_support_enabled.name',
|
||||
ticketSupportResponseTime: 'email_ticket_support.name',
|
||||
mfaEnabled: 'mfa_enabled.name',
|
||||
organizationsEnabled: 'organizations_enabled.name',
|
||||
|
@ -49,7 +48,6 @@ export const quotaItemUnlimitedPhrasesMap: Record<
|
|||
scopesPerRoleLimit: 'scopes_per_role_limit.unlimited',
|
||||
hooksLimit: 'hooks_limit.unlimited',
|
||||
auditLogsRetentionDays: 'audit_logs_retention_days.unlimited',
|
||||
communitySupportEnabled: 'community_support_enabled.unlimited',
|
||||
ticketSupportResponseTime: 'email_ticket_support.unlimited',
|
||||
mfaEnabled: 'mfa_enabled.unlimited',
|
||||
organizationsEnabled: 'organizations_enabled.unlimited',
|
||||
|
@ -76,7 +74,6 @@ export const quotaItemLimitedPhrasesMap: Record<
|
|||
scopesPerRoleLimit: 'scopes_per_role_limit.limited',
|
||||
hooksLimit: 'hooks_limit.limited',
|
||||
auditLogsRetentionDays: 'audit_logs_retention_days.limited',
|
||||
communitySupportEnabled: 'community_support_enabled.limited',
|
||||
ticketSupportResponseTime: 'email_ticket_support.limited',
|
||||
mfaEnabled: 'mfa_enabled.limited',
|
||||
organizationsEnabled: 'organizations_enabled.limited',
|
||||
|
@ -103,18 +100,8 @@ export const quotaItemNotEligiblePhrasesMap: Record<
|
|||
scopesPerRoleLimit: 'scopes_per_role_limit.not_eligible',
|
||||
hooksLimit: 'hooks_limit.not_eligible',
|
||||
auditLogsRetentionDays: 'audit_logs_retention_days.not_eligible',
|
||||
communitySupportEnabled: 'community_support_enabled.not_eligible',
|
||||
ticketSupportResponseTime: 'email_ticket_support.not_eligible',
|
||||
mfaEnabled: 'mfa_enabled.not_eligible',
|
||||
organizationsEnabled: 'organizations_enabled.not_eligible',
|
||||
ssoEnabled: 'sso_enabled.not_eligible',
|
||||
};
|
||||
|
||||
export const quotaItemAddOnPhrasesMap: Partial<
|
||||
Record<
|
||||
keyof SubscriptionPlanQuota,
|
||||
TFuncKey<'translation', 'admin_console.subscription.quota_item'>
|
||||
>
|
||||
> = {
|
||||
machineToMachineLimit: 'machine_to_machine_limit.add_on',
|
||||
};
|
||||
|
|
|
@ -64,7 +64,6 @@ export const defaultSubscriptionPlan: SubscriptionPlan = {
|
|||
hooksLimit: null,
|
||||
organizationsEnabled: true,
|
||||
ssoEnabled: true,
|
||||
communitySupportEnabled: true,
|
||||
ticketSupportResponseTime: 48,
|
||||
thirdPartyApplicationsLimit: null,
|
||||
},
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
&.notCapable {
|
||||
color: var(--color-error);
|
||||
}
|
||||
}
|
||||
|
||||
.lineThrough {
|
||||
text-decoration: line-through;
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
import { cond } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import DescendArrow from '@/assets/icons/descend-arrow.svg';
|
||||
import Failed from '@/assets/icons/failed.svg';
|
||||
import QuotaListItem from '@/components/PlanQuotaList/QuotaListItem';
|
||||
import { type SubscriptionPlanQuota } from '@/types/subscriptions';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
quotaKey: keyof SubscriptionPlanQuota;
|
||||
quotaValue: SubscriptionPlanQuota[keyof SubscriptionPlanQuota];
|
||||
isForDowngradeTargetPlan?: boolean;
|
||||
};
|
||||
|
||||
function DiffQuotaItem({ quotaKey, quotaValue, isForDowngradeTargetPlan }: Props) {
|
||||
const isNotCapable = quotaValue === 0 || quotaValue === false;
|
||||
const DowngradeStatusIcon = isNotCapable ? Failed : DescendArrow;
|
||||
|
||||
return (
|
||||
<QuotaListItem
|
||||
quotaKey={quotaKey}
|
||||
quotaValue={quotaValue}
|
||||
icon={cond(
|
||||
isForDowngradeTargetPlan && (
|
||||
<DowngradeStatusIcon
|
||||
className={classNames(styles.icon, isNotCapable && styles.notCapable)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
phraseClassName={cond(isNotCapable && styles.lineThrough)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default DiffQuotaItem;
|
|
@ -4,7 +4,6 @@ import {
|
|||
quotaItemUnlimitedPhrasesMap,
|
||||
quotaItemPhrasesMap,
|
||||
quotaItemLimitedPhrasesMap,
|
||||
quotaItemAddOnPhrasesMap,
|
||||
} from '@/consts/quota-item-phrases';
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
import { type SubscriptionPlanQuota } from '@/types/subscriptions';
|
||||
|
@ -14,21 +13,17 @@ const quotaItemPhraseKeyPrefix = 'subscription.quota_item';
|
|||
type Props = {
|
||||
quotaKey: keyof SubscriptionPlanQuota;
|
||||
quotaValue: SubscriptionPlanQuota[keyof SubscriptionPlanQuota];
|
||||
isAddOn?: boolean;
|
||||
};
|
||||
|
||||
function QuotaItemPhrase({ quotaKey, quotaValue, isAddOn = false }: Props) {
|
||||
function QuotaItemPhrase({ quotaKey, quotaValue }: Props) {
|
||||
const isUnlimited = quotaValue === null;
|
||||
const isNotCapable = quotaValue === 0 || quotaValue === false;
|
||||
const isLimited = Boolean(quotaValue);
|
||||
|
||||
const limitedPhraseKey =
|
||||
cond(isAddOn && quotaItemAddOnPhrasesMap[quotaKey]) ?? quotaItemLimitedPhrasesMap[quotaKey];
|
||||
|
||||
const phraseKey =
|
||||
cond(isUnlimited && quotaItemUnlimitedPhrasesMap[quotaKey]) ??
|
||||
cond(isNotCapable && quotaItemPhrasesMap[quotaKey]) ??
|
||||
cond(isLimited && limitedPhraseKey);
|
||||
cond(isLimited && quotaItemLimitedPhrasesMap[quotaKey]);
|
||||
|
||||
if (!phraseKey) {
|
||||
// Should not happen
|
|
@ -11,6 +11,19 @@
|
|||
gap: _.unit(2);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
&.notCapable {
|
||||
color: var(--color-error);
|
||||
}
|
||||
}
|
||||
|
||||
.lineThrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
&.withIcon {
|
||||
list-style-type: none;
|
||||
// Unset a margin to the left for the list item marker
|
|
@ -0,0 +1,43 @@
|
|||
import { cond } from '@silverhand/essentials';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import DescendArrow from '@/assets/icons/descend-arrow.svg';
|
||||
import Failed from '@/assets/icons/failed.svg';
|
||||
import { type SubscriptionPlanQuota } from '@/types/subscriptions';
|
||||
|
||||
import QuotaItemPhrase from './QuotaItemPhrase';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
quotaKey: keyof SubscriptionPlanQuota;
|
||||
quotaValue: SubscriptionPlanQuota[keyof SubscriptionPlanQuota];
|
||||
hasStatusIcon?: boolean;
|
||||
};
|
||||
|
||||
function DiffQuotaItem({ quotaKey, quotaValue, hasStatusIcon }: Props) {
|
||||
const isNotCapable = quotaValue === 0 || quotaValue === false;
|
||||
const DowngradeStatusIcon = isNotCapable ? Failed : DescendArrow;
|
||||
|
||||
return (
|
||||
<li className={classNames(styles.quotaListItem, hasStatusIcon && styles.withIcon)}>
|
||||
{/**
|
||||
* Add a `span` as a wrapper to apply the flex layout to the content.
|
||||
* If we apply the flex layout to the `li` directly, the `li` circle bullet will disappear.
|
||||
*/}
|
||||
<span className={styles.content}>
|
||||
{cond(
|
||||
hasStatusIcon && (
|
||||
<DowngradeStatusIcon
|
||||
className={classNames(styles.icon, isNotCapable && styles.notCapable)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<span className={cond(isNotCapable && styles.lineThrough)}>
|
||||
<QuotaItemPhrase quotaKey={quotaKey} quotaValue={quotaValue} />
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default DiffQuotaItem;
|
|
@ -0,0 +1,29 @@
|
|||
import classNames from 'classnames';
|
||||
|
||||
import { type SubscriptionPlanQuotaEntries } from '@/types/subscriptions';
|
||||
|
||||
import DiffQuotaItem from './DiffQuotaItem';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
entries: SubscriptionPlanQuotaEntries;
|
||||
isDowngradeTargetPlan: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function PlanQuotaList({ entries, isDowngradeTargetPlan, className }: Props) {
|
||||
return (
|
||||
<ul className={classNames(styles.planQuotaList, className)}>
|
||||
{entries.map(([quotaKey, quotaValue]) => (
|
||||
<DiffQuotaItem
|
||||
key={quotaKey}
|
||||
quotaKey={quotaKey}
|
||||
quotaValue={quotaValue}
|
||||
hasStatusIcon={isDowngradeTargetPlan}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlanQuotaList;
|
|
@ -1,14 +1,16 @@
|
|||
import { useMemo } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import PlanName from '@/components/PlanName';
|
||||
import PlanQuotaList from '@/components/PlanQuotaList';
|
||||
import { planQuotaItemOrder } from '@/consts/plan-quotas';
|
||||
import DynamicT from '@/ds-components/DynamicT';
|
||||
import {
|
||||
type SubscriptionPlanQuotaEntries,
|
||||
type SubscriptionPlanQuota,
|
||||
} from '@/types/subscriptions';
|
||||
import { sortBy } from '@/utils/sort';
|
||||
|
||||
import DiffQuotaItem from './DiffQuotaItem';
|
||||
import PlanQuotaList from './PlanQuotaList';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
type Props = {
|
||||
|
@ -18,6 +20,17 @@ type Props = {
|
|||
};
|
||||
|
||||
function PlanQuotaDiffCard({ planName, quotaDiff, isDowngradeTargetPlan = false }: Props) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const sortedEntries = useMemo(
|
||||
() =>
|
||||
Object.entries(quotaDiff)
|
||||
.slice()
|
||||
.sort(([preQuotaKey], [nextQuotaKey]) =>
|
||||
sortBy(planQuotaItemOrder)(preQuotaKey, nextQuotaKey)
|
||||
),
|
||||
[quotaDiff]
|
||||
) as SubscriptionPlanQuotaEntries;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.title}>
|
||||
|
@ -31,20 +44,7 @@ function PlanQuotaDiffCard({ planName, quotaDiff, isDowngradeTargetPlan = false
|
|||
/>
|
||||
</Trans>
|
||||
</div>
|
||||
<PlanQuotaList
|
||||
entries={
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
Object.entries(quotaDiff) as SubscriptionPlanQuotaEntries
|
||||
}
|
||||
itemRenderer={(quotaKey, quotaValue) => (
|
||||
<DiffQuotaItem
|
||||
key={quotaKey}
|
||||
quotaKey={quotaKey}
|
||||
quotaValue={quotaValue}
|
||||
isForDowngradeTargetPlan={isDowngradeTargetPlan}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<PlanQuotaList entries={sortedEntries} isDowngradeTargetPlan={isDowngradeTargetPlan} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ import * as styles from './index.module.scss';
|
|||
|
||||
const excludedQuotaKeys = new Set<keyof SubscriptionPlanQuota>([
|
||||
'auditLogsRetentionDays',
|
||||
'communitySupportEnabled',
|
||||
'ticketSupportResponseTime',
|
||||
]);
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { type Nullable } from '@silverhand/essentials';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type InvoicesResponse, type SubscriptionPlanResponse } from '@/cloud/types/router';
|
||||
|
@ -14,8 +13,7 @@ export type SubscriptionPlanQuota = Omit<
|
|||
SubscriptionPlanResponse['quota'],
|
||||
'builtInEmailConnectorEnabled'
|
||||
> & {
|
||||
// Support
|
||||
communitySupportEnabled: boolean;
|
||||
// Add ticket support quota item to the plan since it will be compared in the downgrade plan notification modal.
|
||||
ticketSupportResponseTime: number;
|
||||
};
|
||||
|
||||
|
@ -27,51 +25,6 @@ export type SubscriptionPlan = Omit<SubscriptionPlanResponse, 'quota'> & {
|
|||
quota: SubscriptionPlanQuota;
|
||||
};
|
||||
|
||||
type SubscriptionPlanTable = Partial<
|
||||
SubscriptionPlanQuota & {
|
||||
// Base quota
|
||||
basePrice: string;
|
||||
// UI and branding
|
||||
customCssEnabled: boolean;
|
||||
appLogoAndFaviconEnabled: boolean;
|
||||
darkModeEnabled: boolean;
|
||||
i18nEnabled: boolean;
|
||||
// User authn
|
||||
passwordSignInEnabled: boolean;
|
||||
passwordlessSignInEnabled: boolean;
|
||||
emailConnectorsEnabled: boolean;
|
||||
smsConnectorsEnabled: boolean;
|
||||
// User management
|
||||
userManagementEnabled: boolean;
|
||||
// Organization
|
||||
allowedUsersPerOrganization: Nullable<number>;
|
||||
invitationEnabled: boolean;
|
||||
orgRolesLimit: Nullable<number>;
|
||||
orgPermissionsLimit: Nullable<number>;
|
||||
justInTimeProvisioningEnabled: boolean;
|
||||
// Support
|
||||
soc2ReportEnabled: boolean;
|
||||
hipaaOrBaaReportEnabled: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
export enum SubscriptionPlanTableGroupKey {
|
||||
base = 'base',
|
||||
applications = 'applications',
|
||||
resources = 'resources',
|
||||
branding = 'branding',
|
||||
userAuthentication = 'userAuthentication',
|
||||
roles = 'roles',
|
||||
auditLogs = 'auditLogs',
|
||||
hooks = 'hooks',
|
||||
organizations = 'organizations',
|
||||
support = 'support',
|
||||
}
|
||||
|
||||
export type SubscriptionPlanTableGroupKeyMap = {
|
||||
[key in SubscriptionPlanTableGroupKey]: Array<keyof Required<SubscriptionPlanTable>>;
|
||||
};
|
||||
|
||||
export const localCheckoutSessionGuard = z.object({
|
||||
state: z.string(),
|
||||
sessionId: z.string(),
|
||||
|
|
|
@ -4,7 +4,7 @@ import dayjs from 'dayjs';
|
|||
|
||||
import { tryReadResponseErrorBody } from '@/cloud/hooks/use-cloud-api';
|
||||
import { type SubscriptionPlanResponse } from '@/cloud/types/router';
|
||||
import { communitySupportEnabledMap, ticketSupportResponseTimeMap } from '@/consts/plan-quotas';
|
||||
import { ticketSupportResponseTimeMap } from '@/consts/plan-quotas';
|
||||
import { featuredPlanIdOrder, featuredPlanIds } from '@/consts/subscriptions';
|
||||
import { type SubscriptionPlanQuota, type SubscriptionPlan } from '@/types/subscriptions';
|
||||
|
||||
|
@ -15,7 +15,9 @@ export const addSupportQuotaToPlan = (subscriptionPlanResponse: SubscriptionPlan
|
|||
...subscriptionPlanResponse,
|
||||
quota: {
|
||||
...quota,
|
||||
communitySupportEnabled: communitySupportEnabledMap[id] ?? false, // Fallback to not supported
|
||||
/**
|
||||
* Manually add this support quota item to the plan since it will be compared in the downgrade plan notification modal.
|
||||
*/
|
||||
ticketSupportResponseTime: ticketSupportResponseTimeMap[id] ?? 0, // Fallback to not supported
|
||||
},
|
||||
};
|
||||
|
|
|
@ -32,7 +32,6 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} Maschine-zu-Maschine-Apps',
|
||||
unlimited: 'Unbegrenzte Maschine-zu-Maschine-Apps',
|
||||
not_eligible: 'Entferne deine Maschine-zu-Maschine-Apps',
|
||||
add_on: 'Zusätzliche Maschine-zu-Maschine-Apps',
|
||||
},
|
||||
third_party_applications_limit: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -133,12 +132,6 @@ const quota_item = {
|
|||
unlimited: 'Unbegrenzte Tage',
|
||||
not_eligible: 'Keine Audit-Logs',
|
||||
},
|
||||
community_support_enabled: {
|
||||
name: 'Community-Support',
|
||||
limited: 'Community-Support',
|
||||
unlimited: 'Community-Support',
|
||||
not_eligible: 'Kein Community-Support',
|
||||
},
|
||||
email_ticket_support: {
|
||||
name: 'E-Mail-Ticket-Support',
|
||||
limited: '{{count, number}} Stunde E-Mail-Ticket-Support',
|
||||
|
|
|
@ -32,7 +32,6 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} machine to machine apps',
|
||||
unlimited: 'Unlimited machine to machine apps',
|
||||
not_eligible: 'Remove your machine to machine apps',
|
||||
add_on: 'Additional machine-to-machine apps',
|
||||
},
|
||||
third_party_applications_limit: {
|
||||
name: 'Third-party apps',
|
||||
|
@ -128,12 +127,6 @@ const quota_item = {
|
|||
unlimited: 'Unlimited days',
|
||||
not_eligible: 'No audit logs',
|
||||
},
|
||||
community_support_enabled: {
|
||||
name: 'Community support',
|
||||
limited: 'Community support',
|
||||
unlimited: 'Community support',
|
||||
not_eligible: 'No community support',
|
||||
},
|
||||
email_ticket_support: {
|
||||
name: 'Email ticket support',
|
||||
limited: '{{count, number}} hour email ticket support',
|
||||
|
|
|
@ -32,7 +32,6 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} aplicaciones de dispositivo a dispositivo',
|
||||
unlimited: 'Aplicaciones de dispositivo a dispositivo ilimitadas',
|
||||
not_eligible: 'Elimine sus aplicaciones de dispositivo a dispositivo',
|
||||
add_on: 'Aplicaciones de máquina a máquina adicionales',
|
||||
},
|
||||
third_party_applications_limit: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -133,12 +132,6 @@ const quota_item = {
|
|||
unlimited: 'Días ilimitados',
|
||||
not_eligible: 'Sin registros de auditoría',
|
||||
},
|
||||
community_support_enabled: {
|
||||
name: 'Soporte de la comunidad',
|
||||
limited: 'Soporte de la comunidad',
|
||||
unlimited: 'Soporte de la comunidad',
|
||||
not_eligible: 'Sin soporte de la comunidad',
|
||||
},
|
||||
email_ticket_support: {
|
||||
name: 'Soporte de boletos de correo electrónico',
|
||||
limited: '{{count, number}} hora de soporte de boletos de correo electrónico',
|
||||
|
|
|
@ -32,7 +32,6 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} applications machine à machine',
|
||||
unlimited: 'Illimité applications machine à machine',
|
||||
not_eligible: 'Supprimez vos applications machine à machine',
|
||||
add_on: 'Applications machine à machine supplémentaires',
|
||||
},
|
||||
third_party_applications_limit: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -133,12 +132,6 @@ const quota_item = {
|
|||
unlimited: 'Jours illimités',
|
||||
not_eligible: "Pas de journalisation d'audit",
|
||||
},
|
||||
community_support_enabled: {
|
||||
name: 'Support de la communauté',
|
||||
limited: 'Support de la communauté',
|
||||
unlimited: 'Support de la communauté',
|
||||
not_eligible: 'Pas de support de la communauté',
|
||||
},
|
||||
email_ticket_support: {
|
||||
name: 'Assistance par ticket de messagerie électronique',
|
||||
limited: "{{count, number}} heure d'assistance par ticket de messagerie électronique",
|
||||
|
|
|
@ -32,7 +32,6 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} applicazioni Machine-to-Machine',
|
||||
unlimited: 'Applicazioni Machine-to-Machine illimitate',
|
||||
not_eligible: 'Rimuovi le tue applicazioni Machine-to-Machine',
|
||||
add_on: 'Applicazioni Machine-to-Machine aggiuntive',
|
||||
},
|
||||
third_party_applications_limit: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -133,12 +132,6 @@ const quota_item = {
|
|||
unlimited: 'Giorni illimitati',
|
||||
not_eligible: 'Nessun log di audit',
|
||||
},
|
||||
community_support_enabled: {
|
||||
name: 'Supporto della comunità',
|
||||
limited: 'Supporto della comunità',
|
||||
unlimited: 'Supporto della comunità',
|
||||
not_eligible: 'Nessun supporto della comunità',
|
||||
},
|
||||
email_ticket_support: {
|
||||
name: 'Supporto tramite ticket email',
|
||||
limited: '{{count, number}} ora di supporto tramite ticket email',
|
||||
|
|
|
@ -32,7 +32,6 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} マシン間アプリケーション',
|
||||
unlimited: '無制限のマシン間アプリケーション',
|
||||
not_eligible: 'マシン間アプリケーションを削除してください',
|
||||
add_on: '追加のマシン間アプリケーション',
|
||||
},
|
||||
third_party_applications_limit: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -133,12 +132,6 @@ const quota_item = {
|
|||
unlimited: '無制限の日数',
|
||||
not_eligible: '監査ログがありません',
|
||||
},
|
||||
community_support_enabled: {
|
||||
name: 'コミュニティサポート',
|
||||
limited: 'コミュニティサポート',
|
||||
unlimited: 'コミュニティサポート',
|
||||
not_eligible: 'コミュニティサポートなし',
|
||||
},
|
||||
email_ticket_support: {
|
||||
name: 'メールチケットサポート',
|
||||
limited: '{{count, number}}時間のメールチケットサポート',
|
||||
|
|
|
@ -32,7 +32,6 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} 기계 간 앱',
|
||||
unlimited: '제한 없는 기계 간 앱',
|
||||
not_eligible: '기계 간 앱을 제거하십시오',
|
||||
add_on: '추가 기계간 앱',
|
||||
},
|
||||
third_party_applications_limit: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -133,12 +132,6 @@ const quota_item = {
|
|||
unlimited: '제한 없는 기간',
|
||||
not_eligible: '감사 로그 없음',
|
||||
},
|
||||
community_support_enabled: {
|
||||
name: '커뮤니티 지원',
|
||||
limited: '커뮤니티 지원',
|
||||
unlimited: '커뮤니티 지원',
|
||||
not_eligible: '커뮤니티 지원 없음',
|
||||
},
|
||||
email_ticket_support: {
|
||||
name: '이메일 지원 티켓',
|
||||
limited: '{{count, number}}시간 이메일 지원 티켓',
|
||||
|
|
|
@ -32,7 +32,6 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} aplikacje machine to machine',
|
||||
unlimited: 'Nieograniczona liczba aplikacji machine to machine',
|
||||
not_eligible: 'Usuń swoje aplikacje machine to machine',
|
||||
add_on: 'Dodatkowe aplikacje machine-to-machine',
|
||||
},
|
||||
third_party_applications_limit: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -133,12 +132,6 @@ const quota_item = {
|
|||
unlimited: 'Nieograniczona liczba dni',
|
||||
not_eligible: 'Brak dzienników audytowych',
|
||||
},
|
||||
community_support_enabled: {
|
||||
name: 'Wsparcie społeczności',
|
||||
limited: 'Wsparcie społeczności',
|
||||
unlimited: 'Wsparcie społeczności',
|
||||
not_eligible: 'Brak wsparcia społecznościowego',
|
||||
},
|
||||
email_ticket_support: {
|
||||
name: 'Wsparcie poprzez e-maile',
|
||||
limited: '{{count, number}} godzina wsparcia poprzez e-maile',
|
||||
|
|
|
@ -32,7 +32,6 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} aplicações de máquina a máquina',
|
||||
unlimited: 'Aplicações de máquina a máquina ilimitadas',
|
||||
not_eligible: 'Remova suas aplicações de máquina a máquina',
|
||||
add_on: 'Aplicativos adicionais de máquina a máquina',
|
||||
},
|
||||
third_party_applications_limit: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -133,12 +132,6 @@ const quota_item = {
|
|||
unlimited: 'Dias ilimitados',
|
||||
not_eligible: 'Nenhum registro de auditoria',
|
||||
},
|
||||
community_support_enabled: {
|
||||
name: 'Suporte da comunidade',
|
||||
limited: 'Suporte da comunidade',
|
||||
unlimited: 'Suporte da comunidade',
|
||||
not_eligible: 'Nenhum suporte da comunidade',
|
||||
},
|
||||
email_ticket_support: {
|
||||
name: 'Suporte de ingressos de e-mail',
|
||||
limited: '{{count, number}} hora de suporte de ingressos de e-mail',
|
||||
|
|
|
@ -32,7 +32,6 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} aplicações de máquina para máquina',
|
||||
unlimited: 'Aplicações de máquina para máquina ilimitadas',
|
||||
not_eligible: 'Remover as tuas aplicações de máquina para máquina',
|
||||
add_on: 'Aplicações adicionais de máquina para máquina',
|
||||
},
|
||||
third_party_applications_limit: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -133,12 +132,6 @@ const quota_item = {
|
|||
unlimited: 'Dias ilimitados',
|
||||
not_eligible: 'Sem registos de auditoria',
|
||||
},
|
||||
community_support_enabled: {
|
||||
name: 'Apoio da comunidade',
|
||||
limited: 'Apoio da comunidade',
|
||||
unlimited: 'Apoio da comunidade',
|
||||
not_eligible: 'Sem apoio da comunidade',
|
||||
},
|
||||
email_ticket_support: {
|
||||
name: 'Suporte por e-mail',
|
||||
limited: '{{count, number}} horas de suporte por e-mail',
|
||||
|
|
|
@ -32,7 +32,6 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} приложения для машин ко машине',
|
||||
unlimited: 'Неограниченное количество приложений для машин ко машине',
|
||||
not_eligible: 'Удалите свои приложения для машин ко машине',
|
||||
add_on: 'Дополнительные приложения для машины ко машине',
|
||||
},
|
||||
third_party_applications_limit: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -133,12 +132,6 @@ const quota_item = {
|
|||
unlimited: 'Неограниченное количество дней',
|
||||
not_eligible: 'Без аудит-логов',
|
||||
},
|
||||
community_support_enabled: {
|
||||
name: 'Поддержка сообщества',
|
||||
limited: 'Поддержка сообщества',
|
||||
unlimited: 'Поддержка сообщества',
|
||||
not_eligible: 'Без поддержки сообщества',
|
||||
},
|
||||
email_ticket_support: {
|
||||
name: 'Поддержка по электронной почте',
|
||||
limited: '{{count, number}} час поддержки по электронной почте',
|
||||
|
|
|
@ -32,7 +32,6 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} makineye makine uygulamalar',
|
||||
unlimited: 'Sınırsız makineye makine uygulamalar',
|
||||
not_eligible: 'Makineye makine uygulamalarınızı kaldırın',
|
||||
add_on: 'Ek makineye makine uygulamaları',
|
||||
},
|
||||
third_party_applications_limit: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -133,12 +132,6 @@ const quota_item = {
|
|||
unlimited: 'Sınırsız günler',
|
||||
not_eligible: 'Denetim günlüğünüz yok',
|
||||
},
|
||||
community_support_enabled: {
|
||||
name: 'Topluluk desteği',
|
||||
limited: 'Topluluk desteği',
|
||||
unlimited: 'Topluluk desteği',
|
||||
not_eligible: 'Topluluk desteği yok',
|
||||
},
|
||||
email_ticket_support: {
|
||||
name: 'E-posta bilet desteği',
|
||||
limited: '{{count, number}} saat e-posta bilet desteği',
|
||||
|
|
|
@ -32,7 +32,6 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} 个机器到机器应用',
|
||||
unlimited: '无限制机器到机器应用',
|
||||
not_eligible: '移除你的机器到机器应用',
|
||||
add_on: '更多机器到机器应用',
|
||||
},
|
||||
third_party_applications_limit: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -133,12 +132,6 @@ const quota_item = {
|
|||
unlimited: '无限制天数',
|
||||
not_eligible: '无审计日志',
|
||||
},
|
||||
community_support_enabled: {
|
||||
name: '社区支持',
|
||||
limited: '社区支持',
|
||||
unlimited: '社区支持',
|
||||
not_eligible: '无社区支持',
|
||||
},
|
||||
email_ticket_support: {
|
||||
name: '电子邮件支持',
|
||||
limited: '{{count, number}} 小时电子邮件支持',
|
||||
|
|
|
@ -32,7 +32,6 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} 個機器對機器應用程式',
|
||||
unlimited: '無限機器對機器應用程式',
|
||||
not_eligible: '刪除您的機器對機器應用程式',
|
||||
add_on: '附加的機器對機器應用程式',
|
||||
},
|
||||
third_party_applications_limit: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -133,12 +132,6 @@ const quota_item = {
|
|||
unlimited: '無限天數',
|
||||
not_eligible: '無審計日誌',
|
||||
},
|
||||
community_support_enabled: {
|
||||
name: '社區支援',
|
||||
limited: '社區支援',
|
||||
unlimited: '社區支援',
|
||||
not_eligible: '無社區支援',
|
||||
},
|
||||
email_ticket_support: {
|
||||
name: '電子郵件票務支援',
|
||||
limited: '{{count, number}} 小時電子郵件票務支援',
|
||||
|
|
|
@ -32,7 +32,6 @@ const quota_item = {
|
|||
limited_other: '{{count, number}} 機器對機器應用程式',
|
||||
unlimited: '不限機器對機器應用程式數',
|
||||
not_eligible: '移除你的機器對機器應用程式',
|
||||
add_on: '額外的機器對機器應用程式',
|
||||
},
|
||||
third_party_applications_limit: {
|
||||
/** UNTRANSLATED */
|
||||
|
@ -133,12 +132,6 @@ const quota_item = {
|
|||
unlimited: '不限天數',
|
||||
not_eligible: '無審計記錄',
|
||||
},
|
||||
community_support_enabled: {
|
||||
name: '社群支援',
|
||||
limited: '社群支援',
|
||||
unlimited: '社群支援',
|
||||
not_eligible: '無社群支援',
|
||||
},
|
||||
email_ticket_support: {
|
||||
name: '電子郵件票務支援',
|
||||
limited: '{{count, number}}小時的電子郵件票務支援',
|
||||
|
|
Loading…
Add table
Reference in a new issue