Skip to content

Commit 6352e82

Browse files
simeng-liCopilot
andauthored
feat(console): add new add-on paywall (#7714)
* feat(console): add application creation add-on paywall add application creation add-on paywall * fix(console): open external link in new tab open enternal link in new tab * fix(console): fix add-on unit price display fix add-on unit price display * feat(console): add roles add-on paywall guard add roles add-on paywall guard * chore: trim empty space Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent e5724ef commit 6352e82

File tree

38 files changed

+398
-169
lines changed

38 files changed

+398
-169
lines changed

packages/console/src/components/ApplicationCreation/CreateForm/Footer/index.tsx

Lines changed: 104 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
77
import QuotaGuardFooter from '@/components/QuotaGuardFooter';
88
import SkuName from '@/components/SkuName';
99
import { officialWebsiteContactPageLink } from '@/consts';
10+
import { isDevFeaturesEnabled } from '@/consts/env';
1011
import { addOnPricingExplanationLink } from '@/consts/external-links';
11-
import { machineToMachineAddOnUnitPrice } from '@/consts/subscriptions';
12+
import {
13+
machineToMachineAddOnUnitPrice,
14+
samlApplicationsAddOnUnitPrice,
15+
thirdPartyApplicationsAddOnUnitPrice,
16+
} from '@/consts/subscriptions';
1217
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
1318
import Button, { LinkButton } from '@/ds-components/Button';
1419
import TextLink from '@/ds-components/TextLink';
@@ -27,6 +32,7 @@ type Props = {
2732
readonly onClickCreate: () => void;
2833
};
2934

35+
// eslint-disable-next-line complexity
3036
function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props) {
3137
const {
3238
currentSku,
@@ -39,19 +45,24 @@ function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props)
3945
hasMachineToMachineAppsReachedLimit,
4046
hasThirdPartyAppsReachedLimit,
4147
hasSamlAppsReachedLimit,
42-
hasSamlAppsSurpassedLimit,
4348
} = useApplicationsUsage();
4449
const {
45-
data: { m2mUpsellNoticeAcknowledged },
50+
data: {
51+
m2mUpsellNoticeAcknowledged,
52+
samlAppsUpsellNoticeAcknowledged,
53+
thirdPartyAppsUpsellNoticeAcknowledged,
54+
},
4655
update,
4756
} = useUserPreferences();
4857

58+
const isPaidTenant = isPaidPlan(planId, isEnterprisePlan);
59+
4960
if (selectedType) {
5061
if (
5162
selectedType === ApplicationType.MachineToMachine &&
5263
hasMachineToMachineAppsReachedLimit &&
5364
// Just in case the enterprise plan has reached the resource limit, we still need to show charge notice.
54-
isPaidPlan(planId, isEnterprisePlan) &&
65+
isPaidTenant &&
5566
!m2mUpsellNoticeAcknowledged
5667
) {
5768
return (
@@ -66,7 +77,7 @@ function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props)
6677
<Trans
6778
components={{
6879
span: <span className={styles.strong} />,
69-
a: <TextLink to={addOnPricingExplanationLink} />,
80+
a: <TextLink targetBlank to={addOnPricingExplanationLink} />,
7081
}}
7182
>
7283
{t('add_on.footer.machine_to_machine_app', {
@@ -97,33 +108,99 @@ function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props)
97108
}
98109

99110
if (selectedType === ApplicationType.SAML && hasSamlAppsReachedLimit) {
100-
return (
101-
<div className={createFormStyles.container}>
102-
<div className={createFormStyles.description}>{t('paywall.saml_applications')}</div>
103-
<LinkButton
104-
targetBlank
105-
size="large"
106-
type="primary"
107-
title="general.contact_us_action"
108-
href={officialWebsiteContactPageLink}
109-
/>
110-
</div>
111-
);
111+
// TODO: remove this dev feature guard after the SAML app add-on feature is available for all plans.
112+
// For paid plan (pro plan), we don't guard the SAML app creation since it's an add-on feature.
113+
if (!isDevFeaturesEnabled || currentSku.id === ReservedPlanId.Free) {
114+
return isDevFeaturesEnabled ? (
115+
<QuotaGuardFooter>
116+
<Trans
117+
components={{
118+
a: <ContactUsPhraseLink />,
119+
}}
120+
>
121+
{t('paywall.saml_applications_add_on')}
122+
</Trans>
123+
</QuotaGuardFooter>
124+
) : (
125+
<div className={createFormStyles.container}>
126+
<div className={createFormStyles.description}>{t('paywall.saml_applications')}</div>
127+
<LinkButton
128+
targetBlank
129+
size="large"
130+
type="primary"
131+
title="general.contact_us_action"
132+
href={officialWebsiteContactPageLink}
133+
/>
134+
</div>
135+
);
136+
}
137+
138+
if (isPaidTenant && !samlAppsUpsellNoticeAcknowledged) {
139+
return (
140+
<AddOnNoticeFooter
141+
isLoading={isLoading}
142+
buttonTitle="applications.create"
143+
onClick={() => {
144+
void update({ samlAppsUpsellNoticeAcknowledged: true });
145+
onClickCreate();
146+
}}
147+
>
148+
<Trans
149+
components={{
150+
span: <span className={styles.strong} />,
151+
a: <TextLink targetBlank to={addOnPricingExplanationLink} />,
152+
}}
153+
>
154+
{t('add_on.footer.saml_apps', {
155+
price: samlApplicationsAddOnUnitPrice,
156+
})}
157+
</Trans>
158+
</AddOnNoticeFooter>
159+
);
160+
}
112161
}
113162

114-
// Third party app is only available for paid plan (pro plan).
115163
if (isThirdParty && hasThirdPartyAppsReachedLimit) {
116-
return (
117-
<QuotaGuardFooter>
118-
<Trans
119-
components={{
120-
a: <ContactUsPhraseLink />,
164+
// TODO: remove this dev feature guard after the SAML app add-on feature is available for all plans.
165+
// For paid plan (pro plan), we don't guard the third-party app creation since it's an add-on feature.
166+
if (!isDevFeaturesEnabled || currentSku.id === ReservedPlanId.Free) {
167+
// Third party app is only available for paid plan (pro plan).
168+
return (
169+
<QuotaGuardFooter>
170+
<Trans
171+
components={{
172+
a: <ContactUsPhraseLink />,
173+
}}
174+
>
175+
{t('paywall.third_party_apps')}
176+
</Trans>
177+
</QuotaGuardFooter>
178+
);
179+
}
180+
181+
if (isPaidTenant && !thirdPartyAppsUpsellNoticeAcknowledged) {
182+
return (
183+
<AddOnNoticeFooter
184+
isLoading={isLoading}
185+
buttonTitle="applications.create"
186+
onClick={() => {
187+
void update({ thirdPartyAppsUpsellNoticeAcknowledged: true });
188+
onClickCreate();
121189
}}
122190
>
123-
{t('paywall.third_party_apps')}
124-
</Trans>
125-
</QuotaGuardFooter>
126-
);
191+
<Trans
192+
components={{
193+
span: <span className={styles.strong} />,
194+
a: <TextLink targetBlank to={addOnPricingExplanationLink} />,
195+
}}
196+
>
197+
{t('add_on.footer.third_party_apps', {
198+
price: thirdPartyApplicationsAddOnUnitPrice,
199+
})}
200+
</Trans>
201+
</AddOnNoticeFooter>
202+
);
203+
}
127204
}
128205

129206
if (hasAppsReachedLimit) {

packages/console/src/components/ApplicationCreation/CreateForm/index.tsx

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { type AdminConsoleKey } from '@logto/phrases';
22
import type { Application } from '@logto/schemas';
33
import { ApplicationType } from '@logto/schemas';
4-
import { conditional } from '@silverhand/essentials';
54
import { type ReactElement, useContext, useMemo } from 'react';
65
import { useController, useForm } from 'react-hook-form';
76
import { toast } from 'react-hot-toast';
@@ -12,7 +11,7 @@ import useSWR, { useSWRConfig } from 'swr';
1211
import { GtagConversionId, reportConversion } from '@/components/Conversion/utils';
1312
import LearnMore from '@/components/LearnMore';
1413
import { pricingLink, defaultPageSize, integrateLogto } from '@/consts';
15-
import { isCloud } from '@/consts/env';
14+
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
1615
import { latestProPlanId } from '@/consts/subscriptions';
1716
import { SubscriptionDataContext } from '@/contexts/SubscriptionDataProvider';
1817
import { LinkButton } from '@/ds-components/Button';
@@ -106,7 +105,61 @@ function CreateForm({
106105
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
107106
const api = useApi();
108107

109-
const { hasMachineToMachineAppsReachedLimit } = useApplicationsUsage();
108+
const {
109+
hasMachineToMachineAppsReachedLimit,
110+
hasSamlAppsReachedLimit,
111+
hasThirdPartyAppsReachedLimit,
112+
} = useApplicationsUsage();
113+
114+
const applicationType = watch('type');
115+
const isThirdPartyApp = watch('isThirdParty');
116+
117+
const paywall = useMemo(() => {
118+
if (isPaidTenant) {
119+
return;
120+
}
121+
122+
if (applicationType === ApplicationType.MachineToMachine) {
123+
return latestProPlanId;
124+
}
125+
126+
// TODO: remove this dev feature guard after the new app add-on feature is available for all plans.
127+
if (isDevFeaturesEnabled && applicationType === ApplicationType.SAML) {
128+
return latestProPlanId;
129+
}
130+
131+
if (isDevFeaturesEnabled && isThirdPartyApp) {
132+
return latestProPlanId;
133+
}
134+
}, [applicationType, isPaidTenant, isThirdPartyApp]);
135+
136+
const hasAddOnTag = useMemo(() => {
137+
if (!isPaidTenant) {
138+
return false;
139+
}
140+
141+
if (applicationType === ApplicationType.MachineToMachine) {
142+
return hasMachineToMachineAppsReachedLimit;
143+
}
144+
145+
// TODO: remove this dev feature guard after the new app add-on feature is available for all plans.
146+
if (isDevFeaturesEnabled && applicationType === ApplicationType.SAML) {
147+
return hasSamlAppsReachedLimit;
148+
}
149+
150+
if (isDevFeaturesEnabled && isThirdPartyApp) {
151+
return hasThirdPartyAppsReachedLimit;
152+
}
153+
154+
return false;
155+
}, [
156+
applicationType,
157+
hasMachineToMachineAppsReachedLimit,
158+
hasSamlAppsReachedLimit,
159+
hasThirdPartyAppsReachedLimit,
160+
isPaidTenant,
161+
isThirdPartyApp,
162+
]);
110163

111164
const onSubmit = handleSubmit(
112165
trySubmitSafe(async (data) => {
@@ -165,14 +218,8 @@ function CreateForm({
165218
<ModalLayout
166219
title="applications.create"
167220
subtitle={subtitleElement}
168-
paywall={conditional(
169-
!isPaidTenant && watch('type') === ApplicationType.MachineToMachine && latestProPlanId
170-
)}
171-
hasAddOnTag={
172-
isPaidTenant &&
173-
watch('type') === ApplicationType.MachineToMachine &&
174-
hasMachineToMachineAppsReachedLimit
175-
}
221+
paywall={paywall}
222+
hasAddOnTag={hasAddOnTag}
176223
size={defaultCreateType ? 'medium' : 'large'}
177224
footer={
178225
!isCloud &&

packages/console/src/hooks/use-user-preferences.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ const userPreferencesGuard = z.object({
2626
enterpriseSsoUpsellNoticeAcknowledged: z.boolean().optional(),
2727
addOnChangesInCurrentCycleNoticeAcknowledged: z.boolean().optional(),
2828
securityFeaturesUpsellNoticeAcknowledged: z.boolean().optional(),
29+
samlAppsUpsellNoticeAcknowledged: z.boolean().optional(),
30+
thirdPartyAppsUpsellNoticeAcknowledged: z.boolean().optional(),
31+
rbacUpsellNoticeAcknowledged: z.boolean().optional(),
2932
/* === Add on feature related fields === */
3033
});
3134

0 commit comments

Comments
 (0)