From e8ed7178c190348634f6a414b298ab6d38b0174a Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Tue, 6 Aug 2024 12:04:49 +0800 Subject: [PATCH] feat: add add-on feature notice/tag --- .../AddOnNoticeFooter/index.module.scss | 17 ++++++ .../components/AddOnNoticeFooter/index.tsx | 30 ++++++++++ .../CreateForm/Footer/index.module.scss | 3 + .../CreateForm/Footer/index.tsx | 40 +++++++++++-- .../ApplicationCreation/CreateForm/index.tsx | 13 +++- .../src/components/FeatureTag/AddOnTag.tsx | 19 ++++++ .../src/components/FeatureTag/index.tsx | 13 +++- .../src/ds-components/ModalLayout/index.tsx | 2 +- .../console/src/hooks/use-user-preferences.ts | 1 + .../components/CreateForm/Footer.tsx | 28 +++++++++ .../components/CreateForm/index.module.scss | 3 + .../components/CreateForm/index.tsx | 12 +++- .../pages/CustomizeJwt/CreateButton/index.tsx | 4 +- .../UpsellNotice/index.module.scss | 52 ++++++++++++++++ .../pages/CustomizeJwt/UpsellNotice/index.tsx | 48 +++++++++++++++ .../src/pages/CustomizeJwt/index.module.scss | 4 ++ .../console/src/pages/CustomizeJwt/index.tsx | 27 +++++++-- .../SsoCreationModal/index.module.scss | 4 ++ .../EnterpriseSso/SsoCreationModal/index.tsx | 39 +++++++++++- .../console/src/pages/EnterpriseSso/index.tsx | 18 ++++-- .../pages/Mfa/MfaForm/UpsellNotice/index.tsx | 52 ++++++++++++++++ .../src/pages/Mfa/MfaForm/index.module.scss | 4 ++ .../console/src/pages/Mfa/MfaForm/index.tsx | 2 + .../src/pages/Mfa/PageWrapper/index.tsx | 17 ++++-- .../src/pages/OrganizationTemplate/index.tsx | 12 +++- .../CreateOrganizationModal/index.module.scss | 3 + .../CreateOrganizationModal/index.tsx | 43 +++++++++++-- .../console/src/pages/Organizations/index.tsx | 12 +++- .../TenantMembers/InviteMemberModal/index.tsx | 60 +++++++++++++++---- .../TenantMembers/index.module.scss | 4 ++ .../admin-console/upsell/add-on.ts | 24 ++++++++ .../admin-console/upsell/add-on.ts | 18 ++++++ .../translation/admin-console/upsell/index.ts | 2 + .../admin-console/upsell/add-on.ts | 24 ++++++++ .../admin-console/upsell/add-on.ts | 24 ++++++++ .../admin-console/upsell/add-on.ts | 24 ++++++++ .../admin-console/upsell/add-on.ts | 24 ++++++++ .../admin-console/upsell/add-on.ts | 24 ++++++++ .../admin-console/upsell/add-on.ts | 24 ++++++++ .../admin-console/upsell/add-on.ts | 24 ++++++++ .../admin-console/upsell/add-on.ts | 24 ++++++++ .../admin-console/upsell/add-on.ts | 24 ++++++++ .../admin-console/upsell/add-on.ts | 24 ++++++++ .../admin-console/upsell/add-on.ts | 24 ++++++++ .../admin-console/upsell/add-on.ts | 24 ++++++++ .../admin-console/upsell/add-on.ts | 24 ++++++++ 46 files changed, 894 insertions(+), 48 deletions(-) create mode 100644 packages/console/src/components/AddOnNoticeFooter/index.module.scss create mode 100644 packages/console/src/components/AddOnNoticeFooter/index.tsx create mode 100644 packages/console/src/components/ApplicationCreation/CreateForm/Footer/index.module.scss create mode 100644 packages/console/src/components/FeatureTag/AddOnTag.tsx create mode 100644 packages/console/src/pages/ApiResources/components/CreateForm/index.module.scss create mode 100644 packages/console/src/pages/CustomizeJwt/UpsellNotice/index.module.scss create mode 100644 packages/console/src/pages/CustomizeJwt/UpsellNotice/index.tsx create mode 100644 packages/console/src/pages/Mfa/MfaForm/UpsellNotice/index.tsx create mode 100644 packages/console/src/pages/Organizations/CreateOrganizationModal/index.module.scss create mode 100644 packages/phrases/src/locales/de/translation/admin-console/upsell/add-on.ts create mode 100644 packages/phrases/src/locales/en/translation/admin-console/upsell/add-on.ts create mode 100644 packages/phrases/src/locales/es/translation/admin-console/upsell/add-on.ts create mode 100644 packages/phrases/src/locales/fr/translation/admin-console/upsell/add-on.ts create mode 100644 packages/phrases/src/locales/it/translation/admin-console/upsell/add-on.ts create mode 100644 packages/phrases/src/locales/ja/translation/admin-console/upsell/add-on.ts create mode 100644 packages/phrases/src/locales/ko/translation/admin-console/upsell/add-on.ts create mode 100644 packages/phrases/src/locales/pl-pl/translation/admin-console/upsell/add-on.ts create mode 100644 packages/phrases/src/locales/pt-br/translation/admin-console/upsell/add-on.ts create mode 100644 packages/phrases/src/locales/pt-pt/translation/admin-console/upsell/add-on.ts create mode 100644 packages/phrases/src/locales/ru/translation/admin-console/upsell/add-on.ts create mode 100644 packages/phrases/src/locales/tr-tr/translation/admin-console/upsell/add-on.ts create mode 100644 packages/phrases/src/locales/zh-cn/translation/admin-console/upsell/add-on.ts create mode 100644 packages/phrases/src/locales/zh-hk/translation/admin-console/upsell/add-on.ts create mode 100644 packages/phrases/src/locales/zh-tw/translation/admin-console/upsell/add-on.ts diff --git a/packages/console/src/components/AddOnNoticeFooter/index.module.scss b/packages/console/src/components/AddOnNoticeFooter/index.module.scss new file mode 100644 index 000000000..4a3dc3faa --- /dev/null +++ b/packages/console/src/components/AddOnNoticeFooter/index.module.scss @@ -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); + } +} diff --git a/packages/console/src/components/AddOnNoticeFooter/index.tsx b/packages/console/src/components/AddOnNoticeFooter/index.tsx new file mode 100644 index 000000000..6f40c55f4 --- /dev/null +++ b/packages/console/src/components/AddOnNoticeFooter/index.tsx @@ -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 ( +
+
{children}
+
+ ); +} + +export default AddOnNoticeFooter; diff --git a/packages/console/src/components/ApplicationCreation/CreateForm/Footer/index.module.scss b/packages/console/src/components/ApplicationCreation/CreateForm/Footer/index.module.scss new file mode 100644 index 000000000..c9daaa784 --- /dev/null +++ b/packages/console/src/components/ApplicationCreation/CreateForm/Footer/index.module.scss @@ -0,0 +1,3 @@ +.strong { + font-weight: 500; +} diff --git a/packages/console/src/components/ApplicationCreation/CreateForm/Footer/index.tsx b/packages/console/src/components/ApplicationCreation/CreateForm/Footer/index.tsx index f19933559..4389a8757 100644 --- a/packages/console/src/components/ApplicationCreation/CreateForm/Footer/index.tsx +++ b/packages/console/src/components/ApplicationCreation/CreateForm/Footer/index.tsx @@ -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 ( + + , + a: , + }} + > + {t('add_on.footer.machine_to_machine_app', { + price: Number(addOnUnitPrice) / 100, + })} + + + ); + } + if ( selectedType === ApplicationType.MachineToMachine && hasMachineToMachineAppsReachedLimit && @@ -42,7 +72,7 @@ function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props) a: , }} > - {t('machine_to_machine_feature')} + {t('paywall.machine_to_machine_feature')} ); @@ -57,7 +87,7 @@ function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props) a: , }} > - {t('third_party_apps')} + {t('paywall.third_party_apps')} ); @@ -72,7 +102,7 @@ function Footer({ selectedType, isLoading, onClickCreate, isThirdParty }: Props) planName: , }} > - {t('applications', { count: quota.applicationsLimit ?? 0 })} + {t('paywall.applications', { count: quota.applicationsLimit ?? 0 })} ); diff --git a/packages/console/src/components/ApplicationCreation/CreateForm/index.tsx b/packages/console/src/components/ApplicationCreation/CreateForm/index.tsx index 7f4ac87e9..097c7cf70 100644 --- a/packages/console/src/components/ApplicationCreation/CreateForm/index.tsx +++ b/packages/console/src/components/ApplicationCreation/CreateForm/index.tsx @@ -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({ defaultValues: { type: defaultCreateType, isThirdParty: isDefaultCreateThirdParty }, }); + const { + currentSubscription: { planId }, + } = useContext(SubscriptionDataContext); const { user } = useCurrentUser(); const { mutate: mutateGlobal } = useSWRConfig(); @@ -115,6 +121,9 @@ function CreateForm({ Add-on; +} + +export default AddOnTag; diff --git a/packages/console/src/components/FeatureTag/index.tsx b/packages/console/src/components/FeatureTag/index.tsx index 75d58d327..152ae00f0 100644 --- a/packages/console/src/components/FeatureTag/index.tsx +++ b/packages/console/src/components/FeatureTag/index.tsx @@ -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 ; + } + return
{plan}
; } diff --git a/packages/console/src/ds-components/ModalLayout/index.tsx b/packages/console/src/ds-components/ModalLayout/index.tsx index 149c05f80..4a5f58742 100644 --- a/packages/console/src/ds-components/ModalLayout/index.tsx +++ b/packages/console/src/ds-components/ModalLayout/index.tsx @@ -17,7 +17,7 @@ export type Props = { readonly className?: string; readonly size?: 'medium' | 'large' | 'xlarge'; readonly headerIcon?: ReactElement; -} & Pick; +} & Pick; function ModalLayout({ children, diff --git a/packages/console/src/hooks/use-user-preferences.ts b/packages/console/src/hooks/use-user-preferences.ts index bd4da9f5f..0c44380f6 100644 --- a/packages/console/src/hooks/use-user-preferences.ts +++ b/packages/console/src/hooks/use-user-preferences.ts @@ -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; diff --git a/packages/console/src/pages/ApiResources/components/CreateForm/Footer.tsx b/packages/console/src/pages/ApiResources/components/CreateForm/Footer.tsx index fcfdc4498..d25ba80aa 100644 --- a/packages/console/src/pages/ApiResources/components/CreateForm/Footer.tsx +++ b/packages/console/src/pages/ApiResources/components/CreateForm/Footer.tsx @@ -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 ( + + , + a: , + }} + > + {t('upsell.add_on.footer.api_resource', { + price: Number(addOnUnitPrice) / 100, + })} + + + ); + } + return (