Skip to content

Commit

Permalink
refactor(console): update cloud API calls
Browse files Browse the repository at this point in the history
  • Loading branch information
darcyYe committed Jul 22, 2024
1 parent 802810e commit 7fb0a2e
Show file tree
Hide file tree
Showing 53 changed files with 1,465 additions and 208 deletions.
2 changes: 1 addition & 1 deletion packages/console/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"devDependencies": {
"@fontsource/roboto-mono": "^5.0.0",
"@jest/types": "^29.5.0",
"@logto/cloud": "0.2.5-a7eedce",
"@logto/cloud": "0.2.5-3b703da",
"@logto/connector-kit": "workspace:^4.0.0",
"@logto/core-kit": "workspace:^2.5.0",
"@logto/elements": "workspace:^0.0.0",
Expand Down
20 changes: 20 additions & 0 deletions packages/console/src/cloud/types/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,30 @@ export type SubscriptionPlanResponse = GuardedResponse<
GetRoutes['/api/subscription-plans']
>[number];

export type LogtoSkuResponse = GetArrayElementType<GuardedResponse<GetRoutes['/api/skus']>>;

export type Subscription = GuardedResponse<GetRoutes['/api/tenants/:tenantId/subscription']>;

/** @deprecated */
export type SubscriptionUsage = GuardedResponse<GetRoutes['/api/tenants/:tenantId/usage']>;

/** ===== Use `New` in the naming to avoid confusion with legacy types ===== */
/** The response of `GET /api/tenants/my/subscription/quota` has the same response type. */
export type NewSubscriptionQuota = GuardedResponse<
GetRoutes['/api/tenants/:tenantId/subscription/quota']
>;

/** The response of `GET /api/tenants/my/subscription/usage` has the same response type. */
export type NewSubscriptionUsage = GuardedResponse<
GetRoutes['/api/tenants/:tenantId/subscription/usage']
>;

/** The response of `GET /api/tenants/my/subscription/usage/:entityName/scopes` has the same response type. */
export type NewSubscriptionScopeUsage = GuardedResponse<
GetRoutes['/api/tenants/:tenantId/subscription/usage/:entityName/scopes']
>;
/** ===== Use `New` in the naming to avoid confusion with legacy types ===== */

export type InvoicesResponse = GuardedResponse<GetRoutes['/api/tenants/:tenantId/invoices']>;

export type InvitationResponse = GuardedResponse<GetRoutes['/api/invitations/:invitationId']>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Trans, useTranslation } from 'react-i18next';
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 useApplicationsUsage from '@/hooks/use-applications-usage';
Expand All @@ -17,7 +18,7 @@ type Props = {
};

function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props) {
const { currentPlan } = useContext(SubscriptionDataContext);
const { currentPlan, currentSku } = useContext(SubscriptionDataContext);
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.upsell.paywall' });
const {
hasAppsReachedLimit,
Expand All @@ -32,7 +33,7 @@ function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props)
selectedType === ApplicationType.MachineToMachine &&
hasMachineToMachineAppsReachedLimit &&
// For paid plan (pro plan), we don't guard the m2m app creation since it's an add-on feature.
planId === ReservedPlanId.Free
(isDevFeaturesEnabled ? currentSku.id : planId) === ReservedPlanId.Free
) {
return (
<QuotaGuardFooter>
Expand Down
5 changes: 3 additions & 2 deletions packages/console/src/components/ChargeNotification/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useContext } from 'react';
import { Trans, useTranslation } from 'react-i18next';

import { newPlansBlogLink } from '@/consts';
import { isDevFeaturesEnabled } from '@/consts/env';
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
import InlineNotification from '@/ds-components/InlineNotification';
import TextLink from '@/ds-components/TextLink';
Expand Down Expand Up @@ -38,7 +39,7 @@ function ChargeNotification({
checkedFlagKey,
}: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.upsell' });
const { currentPlan } = useContext(SubscriptionDataContext);
const { currentPlan, currentSku } = useContext(SubscriptionDataContext);
const { configs, updateConfigs } = useConfigs();

// Display null when loading
Expand All @@ -52,7 +53,7 @@ function ChargeNotification({
Boolean(checkedChargeNotification?.[checkedFlagKey]) ||
!hasSurpassedLimit ||
// No charge notification for free plan
currentPlan.id === ReservedPlanId.Free
(isDevFeaturesEnabled ? currentSku.id : currentPlan.id) === ReservedPlanId.Free
) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ import {
type ConnectorFactoryResponse,
ReservedPlanId,
} from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useContext, useMemo } from 'react';
import { Trans, useTranslation } from 'react-i18next';

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 { type ConnectorGroup } from '@/types/connector';
import { hasReachedQuotaLimit } from '@/utils/quota';
import { hasReachedQuotaLimit, hasReachedSubscriptionQuotaLimit } from '@/utils/quota';

type Props = {
readonly isCreatingSocialConnector: boolean;
Expand All @@ -31,35 +33,51 @@ function Footer({
onClickCreateButton,
}: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.upsell.paywall' });
const { currentPlan } = useContext(SubscriptionDataContext);
const { currentPlan, currentSku, currentSubscriptionUsage, currentSubscriptionQuota } =
useContext(SubscriptionDataContext);

const standardConnectorCount = useMemo(
() =>
existingConnectors.filter(
({ isStandard, isDemo, type }) => isStandard && !isDemo && type === ConnectorType.Social
).length,
isDevFeaturesEnabled
? // No more standard connector limit in new pricing model.
0
: existingConnectors.filter(
({ isStandard, isDemo, type }) => isStandard && !isDemo && type === ConnectorType.Social
).length,
[existingConnectors]
);

const socialConnectorCount = useMemo(
() =>
existingConnectors.filter(
({ isStandard, isDemo, type }) => !isStandard && !isDemo && type === ConnectorType.Social
).length,
[existingConnectors]
isDevFeaturesEnabled
? currentSubscriptionUsage.socialConnectorsLimit
: existingConnectors.filter(
({ isStandard, isDemo, type }) =>
!isStandard && !isDemo && type === ConnectorType.Social
).length,
[existingConnectors, currentSubscriptionUsage.socialConnectorsLimit]
);

const isStandardConnectorsReachLimit = hasReachedQuotaLimit({
quotaKey: 'standardConnectorsLimit',
plan: currentPlan,
usage: standardConnectorCount,
});
const isStandardConnectorsReachLimit = isDevFeaturesEnabled
? // No more standard connector limit in new pricing model.
false
: hasReachedQuotaLimit({
quotaKey: 'standardConnectorsLimit',
plan: currentPlan,
usage: standardConnectorCount,
});

const isSocialConnectorsReachLimit = hasReachedQuotaLimit({
quotaKey: 'socialConnectorsLimit',
plan: currentPlan,
usage: socialConnectorCount,
});
const isSocialConnectorsReachLimit = isDevFeaturesEnabled
? hasReachedSubscriptionQuotaLimit({
quotaKey: 'socialConnectorsLimit',
usage: currentSubscriptionUsage.socialConnectorsLimit,
quota: currentSubscriptionQuota,
})
: hasReachedQuotaLimit({
quotaKey: 'socialConnectorsLimit',
plan: currentPlan,
usage: socialConnectorCount,
});

if (isCreatingSocialConnector && selectedConnectorGroup) {
const { id: planId, name: planName, quota } = currentPlan;
Expand All @@ -70,13 +88,19 @@ function Footer({
<Trans
components={{
a: <ContactUsPhraseLink />,
planName: <PlanName name={planName} />,
planName: (
<PlanName
name={conditional(isDevFeaturesEnabled && currentPlan.name) ?? planName}
/>
),
}}
>
{quota.standardConnectorsLimit === 0
? t('standard_connectors_feature')
: t(
planId === ReservedPlanId.Pro ? 'standard_connectors_pro' : 'standard_connectors',
(isDevFeaturesEnabled ? currentSku.id : planId) === ReservedPlanId.Pro
? 'standard_connectors_pro'
: 'standard_connectors',
{
count: quota.standardConnectorsLimit ?? 0,
}
Expand All @@ -92,11 +116,18 @@ function Footer({
<Trans
components={{
a: <ContactUsPhraseLink />,
planName: <PlanName name={planName} />,
planName: (
<PlanName
name={conditional(isDevFeaturesEnabled && currentPlan.name) ?? planName}
/>
),
}}
>
{t('social_connectors', {
count: quota.socialConnectorsLimit ?? 0,
count:
(isDevFeaturesEnabled
? currentSubscriptionQuota.socialConnectorsLimit
: quota.socialConnectorsLimit) ?? 0,
})}
</Trans>
</QuotaGuardFooter>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
@use '@/scss/underscore' as _;

.list {
flex: 1;
margin-block: 0;
padding-inline: 0;
padding-bottom: _.unit(8);
list-style: none;

> li {
display: flex;
font: var(--font-body-2);
align-items: center;
gap: _.unit(2);

.icon {
width: 16px;
height: 16px;

&.failed {
color: var(--color-on-error-container);
}

&.success {
color: var(--color-on-success-container);
}
}

&:not(:first-child) {
margin-top: _.unit(3);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import classNames from 'classnames';

import Failed from '@/assets/icons/failed.svg';
import Success from '@/assets/icons/success.svg';

import * as styles from './index.module.scss';
import useFeaturedSkuContent from './use-featured-sku-content';

type Props = {
readonly skuId: string;
};

function FeaturedSkuContent({ skuId }: Props) {
const contentData = useFeaturedSkuContent(skuId);

return (
<ul className={styles.list}>
{contentData.map(({ title, isAvailable }) => {
return (
<li key={title}>
{isAvailable ? (
<Success className={classNames(styles.icon, styles.success)} />
) : (
<Failed className={classNames(styles.icon, styles.failed)} />
)}
{title}
</li>
);
})}
</ul>
);
}

export default FeaturedSkuContent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { ReservedPlanId } from '@logto/schemas';
import { cond } from '@silverhand/essentials';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';

import {
freePlanAuditLogsRetentionDays,
freePlanM2mLimit,
freePlanMauLimit,
freePlanPermissionsLimit,
freePlanRoleLimit,
proPlanAuditLogsRetentionDays,
} from '@/consts/subscriptions';

type ContentData = {
title: string;
isAvailable: boolean;
};

const useFeaturedSkuContent = (skuId: string) => {
const { t } = useTranslation(undefined, {
keyPrefix: 'admin_console.upsell.featured_plan_content',
});

const contentData: ContentData[] = useMemo(() => {
const isFreePlan = skuId === ReservedPlanId.Free;
const planPhraseKey = isFreePlan ? 'free_plan' : 'pro_plan';

return [
{
title: t(`mau.${planPhraseKey}`, { ...cond(isFreePlan && { count: freePlanMauLimit }) }),
isAvailable: true,
},
{
title: t(`m2m.${planPhraseKey}`, { ...cond(isFreePlan && { count: freePlanM2mLimit }) }),
isAvailable: true,
},
{
title: t('third_party_apps'),
isAvailable: !isFreePlan,
},
{
title: t('mfa'),
isAvailable: !isFreePlan,
},
{
title: t('sso'),
isAvailable: !isFreePlan,
},
{
title: t(`role_and_permissions.${planPhraseKey}`, {
...cond(
isFreePlan && {
roleCount: freePlanRoleLimit,
permissionCount: freePlanPermissionsLimit,
}
),
}),
isAvailable: true,
},
{
title: t('organizations'),
isAvailable: !isFreePlan,
},
{
title: t('audit_logs', {
count: isFreePlan ? freePlanAuditLogsRetentionDays : proPlanAuditLogsRetentionDays,
}),
isAvailable: true,
},
];
}, [t, skuId]);

return contentData;
};

export default useFeaturedSkuContent;
Loading

0 comments on commit 7fb0a2e

Please sign in to comment.