From 92bd3d176bad0cd13362a133fa942312c03c45ba Mon Sep 17 00:00:00 2001 From: Alexander-Turkin Date: Thu, 5 Mar 2026 11:22:54 +0500 Subject: [PATCH 1/8] feat(condo): DOMA-12907 update tooltip content --- .../components/Comments/CommentForm.tsx | 40 ++-- .../Comments/CommentsTabContent.tsx | 53 ++--- .../components/TourPage/TourStepCard.tsx | 39 ++-- .../subscription/components/FeatureGate.tsx | 30 +++ .../components/NoSubscriptionTooltip.tsx | 204 ++++++++++++++++-- .../SubscriptionPlanCard.tsx | 25 +-- .../SubscriptionSettingsContent.tsx | 124 ++--------- .../components/TrialActivatedTooltip.tsx | 49 +++++ .../domains/subscription/components/index.ts | 1 + .../condo/domains/subscription/hooks/index.ts | 1 + .../hooks/useActivateSubscriptions.tsx | 132 ++++++++++++ .../components/BaseTicketForm/index.tsx | 45 ++-- .../IncidentForm/BaseIncidentForm.tsx | 74 ++++--- .../IncidentForm/CreateIncidentForm.tsx | 53 +++-- .../hooks/useIncidentUpdateStatusModal.tsx | 56 +++-- apps/condo/lang/en/en.json | 6 +- apps/condo/lang/es/es.json | 6 +- apps/condo/lang/ru/ru.json | 6 +- apps/condo/pages/_app.tsx | 8 +- apps/condo/pages/incident/[id]/index.tsx | 32 ++- 20 files changed, 666 insertions(+), 318 deletions(-) create mode 100644 apps/condo/domains/subscription/components/FeatureGate.tsx create mode 100644 apps/condo/domains/subscription/components/TrialActivatedTooltip.tsx create mode 100644 apps/condo/domains/subscription/hooks/useActivateSubscriptions.tsx diff --git a/apps/condo/domains/common/components/Comments/CommentForm.tsx b/apps/condo/domains/common/components/Comments/CommentForm.tsx index f283f4e4dee..1b426af7f01 100644 --- a/apps/condo/domains/common/components/Comments/CommentForm.tsx +++ b/apps/condo/domains/common/components/Comments/CommentForm.tsx @@ -12,7 +12,7 @@ import { FormWithAction } from '@condo/domains/common/components/containers/Form import { Module, useMultipleFileUploadHook } from '@condo/domains/common/components/MultipleFileUpload' import { useValidations } from '@condo/domains/common/hooks/useValidations' import { analytics } from '@condo/domains/common/utils/analytics' -import { NoSubscriptionTooltip } from '@condo/domains/subscription/components' +import { FeatureGate } from '@condo/domains/subscription/components' import { useOrganizationSubscription } from '@condo/domains/subscription/hooks' import { GENERATE_COMMENT_TOUR_STEP_CLOSED_COOKIE, UPDATE_COMMENT_TOUR_STEP_CLOSED_COOKIE } from '@condo/domains/ticket/constants/common' @@ -405,7 +405,25 @@ const CommentForm: React.FC = ({ /> , ...(rewriteCommentEnabled ? [ - hasAiFeature ? ( + commentsContainerRef.current} + fallback={
+ +
} + > = ({ rewriteTextLoading={rewriteTextLoading} onClick={handleUpdateComment} /> - ) : ( - -
- -
-
- ), +
, ] : []), ]} /> diff --git a/apps/condo/domains/common/components/Comments/CommentsTabContent.tsx b/apps/condo/domains/common/components/Comments/CommentsTabContent.tsx index f61cb64c687..0cfd25c77d0 100644 --- a/apps/condo/domains/common/components/Comments/CommentsTabContent.tsx +++ b/apps/condo/domains/common/components/Comments/CommentsTabContent.tsx @@ -12,8 +12,7 @@ import { Tooltip, Tour, Typography } from '@open-condo/ui' import { AIFlowButton } from '@condo/domains/ai/components/AIFlowButton' import { Loader } from '@condo/domains/common/components/Loader' -import { NoSubscriptionTooltip } from '@condo/domains/subscription/components' -import { useOrganizationSubscription } from '@condo/domains/subscription/hooks' +import { FeatureGate } from '@condo/domains/subscription/components' import { GENERATE_COMMENT_TOUR_STEP_CLOSED_COOKIE } from '@condo/domains/ticket/constants/common' import { Comment } from './Comment' @@ -80,8 +79,6 @@ export const CommentsTabContent: React.FC = ({ const GenerateCommentTourStepTitle = intl.formatMessage({ id: 'ai.generateComment.tourStepTitle' }) const GenerateCommentTourStepDescription = intl.formatMessage({ id: 'ai.generateComment.tourStepDescription' }) - const { isFeatureAvailable } = useOrganizationSubscription() - const hasAiFeature = isFeatureAvailable('ai') const lastComment = useMemo(() => comments?.[0], [comments]) const showGenerateAnswerButton = useMemo(() => @@ -114,7 +111,18 @@ export const CommentsTabContent: React.FC = ({ /> { showGenerateAnswerButton && lastComment?.id === comment.id && ( - hasAiFeature ? ( + + + {GenerateResponseMessage} + + + } + >
= ({
- ) : ( - -
- - {GenerateResponseMessage} - -
-
- ) +
) } @@ -141,7 +141,7 @@ export const CommentsTabContent: React.FC = ({ }), [ GenerateResponseMessage, GenerateResponseTooltipMessage, comments, editableComment, generateCommentLoading, generateCommentOnClickHandler, lastComment?.id, setEditableComment, - showGenerateAnswerButton, updateAction, hasAiFeature, + showGenerateAnswerButton, updateAction, ]) const { currentStep, setCurrentStep } = Tour.useTourContext() @@ -172,7 +172,18 @@ export const CommentsTabContent: React.FC = ({ PromptDescriptionMessage={PromptDescriptionMessage} AiButton={
{showGenerateCommentWithoutComments && ( - hasAiFeature ? ( + + + {GenerateCommentMessage} + +
+ } + > = ({ - ) : ( - -
- - {GenerateCommentMessage} - -
-
- ) + )} } /> diff --git a/apps/condo/domains/onboarding/components/TourPage/TourStepCard.tsx b/apps/condo/domains/onboarding/components/TourPage/TourStepCard.tsx index 2c506aa5739..9c24bfbce27 100644 --- a/apps/condo/domains/onboarding/components/TourPage/TourStepCard.tsx +++ b/apps/condo/domains/onboarding/components/TourPage/TourStepCard.tsx @@ -16,7 +16,7 @@ import { COMPLETED_STEP_LINK, TOUR_STEP_ACTION_PERMISSION, } from '@condo/domains/onboarding/utils/clientSchema/constants' -import { NoSubscriptionTooltip } from '@condo/domains/subscription/components' +import { FeatureGate } from '@condo/domains/subscription/components' import { useOrganizationSubscription } from '@condo/domains/subscription/hooks' import type { AvailableFeature } from '@condo/domains/subscription/constants/features' @@ -125,36 +125,27 @@ export const TourStepCard: React.FC = (props) => { return CompletePreviousStepMessage }, [CompletePreviousStepMessage, NoPermissionsMessage, SettingsMessage, hasPermission]) - const isDisabledStatus = useMemo(() => stepStatus === TourStepStatusType.Disabled || !hasPermission || !hasRequiredFeature || disabled, [disabled, hasPermission, hasRequiredFeature, stepStatus]) + const isDisabledStatus = useMemo(() => stepStatus === TourStepStatusType.Disabled || !hasPermission || disabled, [disabled, hasPermission, stepStatus]) + const isDisabledWithRequiredFeature = isDisabledStatus || !hasRequiredFeature const cardContent = ( ) - if (!hasRequiredFeature) { - return ( - -
- {cardContent} -
-
- ) - } - if (isDisabledStatus) { return ( @@ -165,5 +156,19 @@ export const TourStepCard: React.FC = (props) => { ) } - return cardContent + return + {cardContent} + + } + > +
+ {cardContent} +
+
} \ No newline at end of file diff --git a/apps/condo/domains/subscription/components/FeatureGate.tsx b/apps/condo/domains/subscription/components/FeatureGate.tsx new file mode 100644 index 00000000000..6e6a2abbe7b --- /dev/null +++ b/apps/condo/domains/subscription/components/FeatureGate.tsx @@ -0,0 +1,30 @@ +import React from 'react' + +import { NoSubscriptionTooltip, NoSubscriptionTooltipProps } from '@condo/domains/subscription/components/NoSubscriptionTooltip' +import { useOrganizationSubscription } from '@condo/domains/subscription/hooks' + +interface FeatureGateProps extends Omit { + children: React.ReactElement + fallback: React.ReactElement + id: string +} + +export const FeatureGate: React.FC = ({ + children, + feature, + fallback, + id, + ...tooltipProps +}) => { + const { isFeatureAvailable } = useOrganizationSubscription() + + const hasFeature = Array.isArray(feature) + ? feature.every(f => isFeatureAvailable(f)) + : isFeatureAvailable(feature) + + return ( + + {hasFeature ? children : fallback} + + ) +} diff --git a/apps/condo/domains/subscription/components/NoSubscriptionTooltip.tsx b/apps/condo/domains/subscription/components/NoSubscriptionTooltip.tsx index e32f65faa40..37cb56c3789 100644 --- a/apps/condo/domains/subscription/components/NoSubscriptionTooltip.tsx +++ b/apps/condo/domains/subscription/components/NoSubscriptionTooltip.tsx @@ -1,45 +1,225 @@ +import { useGetAvailableSubscriptionPlansQuery } from '@app/condo/gql' import { useRouter } from 'next/router' -import React, { useCallback } from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import { useIntl } from '@open-condo/next/intl' -import { Button, Space, Tooltip, Typography } from '@open-condo/ui' -import type { TooltipProps } from '@open-condo/ui' +import { useOrganization } from '@open-condo/next/organization' +import { Button, Space, Tooltip, Tour, Typography } from '@open-condo/ui' +import { CURRENCY_SYMBOLS } from '@condo/domains/common/constants/currencies' import { SETTINGS_TAB_SUBSCRIPTION } from '@condo/domains/common/constants/settingsTabs' +import { TrialActivatedTooltip } from '@condo/domains/subscription/components/TrialActivatedTooltip' +import { getRequiredFeature } from '@condo/domains/subscription/constants/routeFeatureMapping' +import { useActivateSubscriptions, useOrganizationSubscription } from '@condo/domains/subscription/hooks' + +import type { AvailableFeature } from '@condo/domains/subscription/constants/features' + +const TRIAL_POPUP_PLAN_KEY = 'active_trial_popup_plan' +const TRIAL_POPUP_INSTANCE_KEY = 'active_trial_popup_instance' export interface NoSubscriptionTooltipProps { children: React.ReactElement - placement?: TooltipProps['placement'] + placement?: 'top' | 'bottom' | 'left' | 'right' + feature?: AvailableFeature | AvailableFeature[] + id?: string + path?: string + getPopupContainer?: () => HTMLElement + skipTooltip?: boolean } -export const NoSubscriptionTooltip: React.FC = ({ children, placement = 'right' }) => { +export const NoSubscriptionTooltip: React.FC = ({ children, placement = 'right', feature: featureProp, path, getPopupContainer, id, skipTooltip }) => { const intl = useIntl() const router = useRouter() + const { organization } = useOrganization() + const { trialSubscriptions, activatedSubscriptions, handleActivatePlan, activateLoading } = useActivateSubscriptions() + const { isFeatureAvailable, hasSubscription } = useOrganizationSubscription() + const [isActivating, setIsActivating] = useState(false) + const [recentlyActivatedTrialPlan, setRecentlyActivatedTrialPlan] = useState(null) + + useEffect(() => { + if (!id) return + + const storedPlan = sessionStorage.getItem(TRIAL_POPUP_PLAN_KEY) + const storedInstance = sessionStorage.getItem(TRIAL_POPUP_INSTANCE_KEY) + + if (storedPlan && storedInstance === id) { + setRecentlyActivatedTrialPlan(storedPlan) + } + }, [id]) - const NoSubscriptionWarning = intl.formatMessage({ + const requiredFeature = path ? getRequiredFeature(path) : null + const feature = (featureProp || requiredFeature) as AvailableFeature | undefined | null + + const FeatureLockedMessage = intl.formatMessage({ id: 'subscription.warns.noActiveSubscription', }) - const ActivateSubscriptionButton = intl.formatMessage({ + const UpgradePlanMessage = intl.formatMessage({ + id: 'subscription.warns.upgradePlan', + }) + const ViewPlansButton = intl.formatMessage({ id: 'subscription.warns.activateSubscriptionButton', }) - const handleActivateClick = useCallback(async () => { + const { data: plansData } = useGetAvailableSubscriptionPlansQuery({ + variables: { + organization: { id: organization?.id }, + }, + skip: !organization?.id, + }) + + const hasActivatedAnyTrial = trialSubscriptions.length > 0 + const isAvailable = feature + ? Array.isArray(feature) + ? feature.every(f => isFeatureAvailable(f)) + : isFeatureAvailable(feature) + : false + + const bestPlanWithFeature = useMemo(() => { + if (!feature || isAvailable) return null + + const plans = plansData?.result?.plans || [] + + const plansWithFeature = plans + .filter(planInfo => { + const plan = planInfo?.plan + if (!plan) return false + + const hasFeature = Array.isArray(feature) + ? feature.every(f => plan[f] === true) + : plan[feature] === true + const hasTrialDays = plan.trialDays > 0 + const prices = planInfo?.prices || [] + const hasPrice = prices.length > 0 + + const alreadyActivated = activatedSubscriptions.some( + ctx => ctx.subscriptionPlan && ( + ctx.subscriptionPlan.id === plan.id || + ctx.subscriptionPlan.priority > (plan.priority || 0) + ) + ) + + return hasFeature && hasTrialDays && hasPrice && !alreadyActivated + }) + .sort((a, b) => (b.plan?.priority ?? 0) - (a.plan?.priority ?? 0)) + + return plansWithFeature[0] || null + }, [feature, isAvailable, plansData?.result?.plans, activatedSubscriptions]) + + const TryTrialButton = useMemo(() => { + const currencyCode = bestPlanWithFeature?.prices?.[0]?.currencyCode + return intl.formatMessage({ + id: 'subscription.warns.tryTrialButton', + }, { currency: CURRENCY_SYMBOLS[currencyCode] || '' }) + }, [bestPlanWithFeature, intl]) + + const handleActivateTrial = useCallback(async () => { + if (!bestPlanWithFeature) { + await router.push(`/settings?tab=${SETTINGS_TAB_SUBSCRIPTION}`) + return + } + + const price = bestPlanWithFeature.prices?.[0] + if (!price?.id) return + + setIsActivating(true) + try { + const planName = bestPlanWithFeature.plan?.name || '' + + await handleActivatePlan({ + priceId: price.id, + isTrial: true, + planName, + trialDays: bestPlanWithFeature.plan?.trialDays || 0, + isCustomPrice: false, + }) + + if (id) { + sessionStorage.setItem(TRIAL_POPUP_PLAN_KEY, planName) + sessionStorage.setItem(TRIAL_POPUP_INSTANCE_KEY, id) + } + setRecentlyActivatedTrialPlan(planName) + } finally { + setIsActivating(false) + } + }, [bestPlanWithFeature, handleActivatePlan, router, id]) + + const handleViewPlans = useCallback(async () => { await router.push(`/settings?tab=${SETTINGS_TAB_SUBSCRIPTION}`) }, [router]) + const isLoading = activateLoading || isActivating + + const buttonText = hasActivatedAnyTrial ? ViewPlansButton : TryTrialButton + + const buttonAction = !feature || hasActivatedAnyTrial ? handleViewPlans : handleActivateTrial + const tooltipTitle = ( - - {NoSubscriptionWarning} - ) + const handleClearRecentlyActivatedTrial = useCallback(() => { + sessionStorage.removeItem(TRIAL_POPUP_PLAN_KEY) + sessionStorage.removeItem(TRIAL_POPUP_INSTANCE_KEY) + setRecentlyActivatedTrialPlan(null) + }, []) + + if (skipTooltip) { + console.log(1, featureProp) + return children + } + + if (path && !requiredFeature) { + console.log(2, featureProp) + + return children + } + + if (path && !hasSubscription) { + console.log(3, featureProp) + + return children + } + + if (recentlyActivatedTrialPlan) { + console.log(4, featureProp) + + return ( + + + {children} + + + ) + } + + if (isAvailable) { + console.log(5, featureProp) + return children + } + console.log(6, featureProp) + return ( {children} diff --git a/apps/condo/domains/subscription/components/SubscriptionSettingsContent/SubscriptionPlanCard/SubscriptionPlanCard.tsx b/apps/condo/domains/subscription/components/SubscriptionSettingsContent/SubscriptionPlanCard/SubscriptionPlanCard.tsx index c1af30f3de5..bba6feea2cb 100644 --- a/apps/condo/domains/subscription/components/SubscriptionSettingsContent/SubscriptionPlanCard/SubscriptionPlanCard.tsx +++ b/apps/condo/domains/subscription/components/SubscriptionSettingsContent/SubscriptionPlanCard/SubscriptionPlanCard.tsx @@ -92,6 +92,7 @@ interface SubscriptionPlanCardProps { b2bAppsMap: Map allB2BAppIds: string[] emoji?: string + trialActivateLoading?: boolean } interface SubscriptionPlanBadgeProps { @@ -172,7 +173,7 @@ const SubscriptionPlanBadge: React.FC = ({ plan, act ) } -export const SubscriptionPlanCard: React.FC = ({ planInfo, activatedTrial, pendingRequest, activatedSubscriptions, handleActivatePlan, b2bAppsMap, allB2BAppIds, emoji }) => { +export const SubscriptionPlanCard: React.FC = ({ planInfo, activatedTrial, pendingRequest, activatedSubscriptions, handleActivatePlan, b2bAppsMap, allB2BAppIds, emoji, trialActivateLoading = false }) => { const intl = useIntl() const RequestPendingMessage = intl.formatMessage({ id: 'subscription.planCard.requestPending' }) const SubmitRequestMessage = intl.formatMessage({ id: 'subscription.planCard.submitRequest' }) @@ -186,7 +187,6 @@ export const SubscriptionPlanCard: React.FC = ({ plan const { useFlagValue } = useFeatureFlags() const { subscriptionContext: activeSubscriptionContext, daysRemaining } = useOrganizationSubscription() const [activateLoading, setActivateLoading] = useState(false) - const [trialActivateLoading, setTrialActivateLoading] = useState(false) const { plan, prices } = planInfo const price = prices?.[0] @@ -290,18 +290,13 @@ export const SubscriptionPlanCard: React.FC = ({ plan const handleTrialActivateClick = useCallback(async () => { if (!price?.id) return - setTrialActivateLoading(true) - try { - await handleActivatePlan({ - priceId: price.id, - isTrial: true, - planName: plan.name, - trialDays: plan.trialDays, - isCustomPrice, - }) - } finally { - setTrialActivateLoading(false) - } + await handleActivatePlan({ + priceId: price.id, + isTrial: true, + planName: plan.name, + trialDays: plan.trialDays, + isCustomPrice, + }) }, [handleActivatePlan, price?.id, plan.name, plan.trialDays, isCustomPrice]) const renderFeature = useCallback(({ featureKey, label, hint }: FeatureConfig) => ( @@ -437,7 +432,7 @@ export const SubscriptionPlanCard: React.FC = ({ plan type='accent' onClick={handleTrialActivateClick} loading={trialActivateLoading} - disabled={!canManageSubscriptions} + disabled={!canManageSubscriptions || trialActivateLoading} > {TryFreeMessage} diff --git a/apps/condo/domains/subscription/components/SubscriptionSettingsContent/SubscriptionSettingsContent.tsx b/apps/condo/domains/subscription/components/SubscriptionSettingsContent/SubscriptionSettingsContent.tsx index c6162d175aa..602a4a690c5 100644 --- a/apps/condo/domains/subscription/components/SubscriptionSettingsContent/SubscriptionSettingsContent.tsx +++ b/apps/condo/domains/subscription/components/SubscriptionSettingsContent/SubscriptionSettingsContent.tsx @@ -1,14 +1,12 @@ -import { useGetAvailableSubscriptionPlansQuery, GetAvailableSubscriptionPlansQueryResult, useGetOrganizationTrialSubscriptionsQuery, useActivateSubscriptionPlanMutation, useGetPendingSubscriptionRequestsQuery, useGetB2BAppsByIdsQuery, useGetOrganizationActivatedSubscriptionsQuery } from '@app/condo/gql' -import { notification } from 'antd' -import dayjs from 'dayjs' +import { useGetAvailableSubscriptionPlansQuery, GetAvailableSubscriptionPlansQueryResult, useGetB2BAppsByIdsQuery } from '@app/condo/gql' import React, { useState, useMemo } from 'react' -import { getClientSideSenderInfo } from '@open-condo/miniapp-utils/helpers/sender' import { useIntl } from '@open-condo/next/intl' import { useOrganization } from '@open-condo/next/organization' -import { Space, Radio, Typography } from '@open-condo/ui' +import { Space, Radio } from '@open-condo/ui' import { Loader } from '@condo/domains/common/components/Loader' +import { useActivateSubscriptions } from '@condo/domains/subscription/hooks' import { PromoBanner } from './PromoBanner/PromoBanner' import { SubscriptionPlanCard } from './SubscriptionPlanCard/SubscriptionPlanCard' @@ -22,12 +20,10 @@ const PLAN_CARD_EMOJIS = ['🏠', '🚀', '👑'] export const SubscriptionSettingsContent: React.FC = () => { const intl = useIntl() - const { organization, employee, selectEmployee } = useOrganization() + const { organization } = useOrganization() const YearlyLabel = intl.formatMessage({ id: 'subscription.period.yearly' }) const MonthlyLabel = intl.formatMessage({ id: 'subscription.period.monthly' }) - const ActivationErrorTitle = intl.formatMessage({ id: 'subscription.activation.errorTitle' }) - const ActivationErrorMessage = intl.formatMessage({ id: 'subscription.activation.error' }) const [planPeriod, setPlanPeriod] = useState('year') @@ -73,108 +69,15 @@ export const SubscriptionSettingsContent: React.FC = () => { }, [b2bAppsData]) const { - data: trialSubscriptionsData, - loading: trialSubscriptionsLoading, - refetch: refetchTrialSubscriptions, - } = useGetOrganizationTrialSubscriptionsQuery({ - variables: { - organizationId: organization?.id, - }, - skip: !organization?.id, - }) - - const { data: pendingRequestsData, loading: pendingRequestsLoading, refetch: refetchPendingRequests } = useGetPendingSubscriptionRequestsQuery({ - variables: { organizationId: organization?.id }, - skip: !organization?.id, - }) - - const { data: activatedSubscriptionsData, loading: activatedSubscriptionsLoading, refetch: refetchActivatedSubscriptions } = useGetOrganizationActivatedSubscriptionsQuery({ - variables: { - organizationId: organization?.id || '', - }, - skip: !organization?.id, - }) - - const trialSubscriptions = trialSubscriptionsData?.trialSubscriptions || [] - const pendingRequests = pendingRequestsData?.pendingRequests || [] - const activatedSubscriptions = activatedSubscriptionsData?.activatedSubscriptions || [] - - const [activateSubscriptionPlan] = useActivateSubscriptionPlanMutation() - - const handleActivatePlan = async ({ priceId, isTrial = true, planName = '', trialDays = 0, isCustomPrice = false }: { - priceId: string - isTrial?: boolean - planName?: string - trialDays?: number - isCustomPrice?: boolean - }) => { - if (!organization) return - - try { - await activateSubscriptionPlan({ - variables: { - data: { - dv: 1, - sender: getClientSideSenderInfo(), - organization: { id: organization.id }, - pricingRule: { id: priceId }, - isTrial, - }, - }, - }) - - if (isTrial) { - await refetchTrialSubscriptions() - await refetchPendingRequests() - await refetchActivatedSubscriptions() - if (employee?.id) { - await selectEmployee(employee.id) - } - notification.success({ - message: ( - - {intl.formatMessage({ id: 'subscription.activation.trial.title' }, { planName })} - - ), - description: intl.formatMessage({ id: 'subscription.activation.trial.description' }, { planName, days: trialDays }), - duration: 5, - }) - } else { - await refetchPendingRequests() - if (isCustomPrice) { - notification.success({ - message: ( - - {intl.formatMessage({ id: 'subscription.activation.paid.custom.title' }, { planName })} - - ), - description: intl.formatMessage({ id: 'subscription.activation.paid.custom.description' }), - duration: 5, - }) - } else { - notification.success({ - message: ( - - {intl.formatMessage({ id: 'subscription.activation.paid.standard.title' })} - - ), - description: intl.formatMessage({ id: 'subscription.activation.paid.standard.description' }), - duration: 5, - }) - } - } - } catch (error) { - console.error('Failed to activate subscription:', error) - notification.error({ - message: ActivationErrorTitle, - description: error?.message || ActivationErrorMessage, - duration: 5, - }) - } - } - - - const isLoading = plansLoading || trialSubscriptionsLoading || pendingRequestsLoading || activatedSubscriptionsLoading + handleActivatePlan, + activateLoading, + trialSubscriptions, + pendingRequests, + activatedSubscriptions, + isLoading: trialActivationLoading, + } = useActivateSubscriptions() + + const isLoading = plansLoading || trialActivationLoading if (isLoading) return return ( @@ -210,6 +113,7 @@ export const SubscriptionSettingsContent: React.FC = () => { b2bAppsMap={b2bAppsMap} allB2BAppIds={allB2BAppIds} emoji={PLAN_CARD_EMOJIS?.[index]} + trialActivateLoading={activateLoading} /> ) })} diff --git a/apps/condo/domains/subscription/components/TrialActivatedTooltip.tsx b/apps/condo/domains/subscription/components/TrialActivatedTooltip.tsx new file mode 100644 index 00000000000..61cbae4e52c --- /dev/null +++ b/apps/condo/domains/subscription/components/TrialActivatedTooltip.tsx @@ -0,0 +1,49 @@ +import React, { useCallback } from 'react' + +import { useIntl } from '@open-condo/next/intl' +import { Tour } from '@open-condo/ui' + +export interface TrialActivatedTooltipProps { + children: React.ReactElement + placement?: 'top' | 'bottom' | 'left' | 'right' + planName: string + onClose?: () => void + getPopupContainer?: () => HTMLElement +} + +export const TrialActivatedTooltip: React.FC = ({ + children, + placement = 'right', + planName, + onClose, + getPopupContainer, +}) => { + const intl = useIntl() + + const WelcomeTitle = intl.formatMessage({ + id: 'subscription.trial.welcome.title', + }, { planName }) + + const WelcomeDescription = intl.formatMessage({ + id: 'subscription.trial.welcome.description', + }, { planName }) + + const handleClose = useCallback(() => { + if (onClose) { + onClose() + } + }, [onClose]) + + return ( + + {children} + + ) +} diff --git a/apps/condo/domains/subscription/components/index.ts b/apps/condo/domains/subscription/components/index.ts index ddfed18db3f..ab315613c43 100644 --- a/apps/condo/domains/subscription/components/index.ts +++ b/apps/condo/domains/subscription/components/index.ts @@ -1,5 +1,6 @@ export { NoSubscriptionTooltip } from './NoSubscriptionTooltip' export type { NoSubscriptionTooltipProps } from './NoSubscriptionTooltip' +export { FeatureGate } from './FeatureGate' export { SubscriptionAccessGuard } from './SubscriptionAccessGuard' export { SubscriptionWelcomeModal } from './SubscriptionWelcomeModal' export { SubscriptionTrialEndedModal } from './SubscriptionTrialEndedModal' diff --git a/apps/condo/domains/subscription/hooks/index.ts b/apps/condo/domains/subscription/hooks/index.ts index fac7d362e08..7ecf21fa152 100644 --- a/apps/condo/domains/subscription/hooks/index.ts +++ b/apps/condo/domains/subscription/hooks/index.ts @@ -1,2 +1,3 @@ export { useOrganizationSubscription } from './useOrganizationSubscription' export type { SubscriptionContext } from './useOrganizationSubscription' +export { useActivateSubscriptions } from './useActivateSubscriptions' diff --git a/apps/condo/domains/subscription/hooks/useActivateSubscriptions.tsx b/apps/condo/domains/subscription/hooks/useActivateSubscriptions.tsx new file mode 100644 index 00000000000..b9fdedf251f --- /dev/null +++ b/apps/condo/domains/subscription/hooks/useActivateSubscriptions.tsx @@ -0,0 +1,132 @@ +import { useActivateSubscriptionPlanMutation, useGetOrganizationTrialSubscriptionsQuery, useGetPendingSubscriptionRequestsQuery, useGetOrganizationActivatedSubscriptionsQuery } from '@app/condo/gql' +import { notification } from 'antd' +import { useCallback, useState } from 'react' + +import { getClientSideSenderInfo } from '@open-condo/miniapp-utils/helpers/sender' +import { useIntl } from '@open-condo/next/intl' +import { useOrganization } from '@open-condo/next/organization' +import { Typography } from '@open-condo/ui' + +interface ActivatePlanParams { + priceId: string + isTrial?: boolean + planName?: string + trialDays?: number + isCustomPrice?: boolean +} + +export const useActivateSubscriptions = () => { + const intl = useIntl() + const { organization, employee, selectEmployee } = useOrganization() + + const ActivationErrorTitle = intl.formatMessage({ id: 'subscription.activation.errorTitle' }) + const ActivationErrorMessage = intl.formatMessage({ id: 'subscription.activation.error' }) + + const [activateLoading, setActivateLoading] = useState(false) + const { + data: trialSubscriptionsData, + loading: trialSubscriptionsLoading, + refetch: refetchTrialSubscriptions, + } = useGetOrganizationTrialSubscriptionsQuery({ + variables: { + organizationId: organization?.id, + }, + skip: !organization?.id, + }) + + const { data: pendingRequestsData, loading: pendingRequestsLoading, refetch: refetchPendingRequests } = useGetPendingSubscriptionRequestsQuery({ + variables: { organizationId: organization?.id }, + skip: !organization?.id, + }) + + const { data: activatedSubscriptionsData, loading: activatedSubscriptionsLoading, refetch: refetchActivatedSubscriptions } = useGetOrganizationActivatedSubscriptionsQuery({ + variables: { + organizationId: organization?.id || '', + }, + skip: !organization?.id, + }) + + const [activateSubscriptionPlan] = useActivateSubscriptionPlanMutation() + + const trialSubscriptions = trialSubscriptionsData?.trialSubscriptions || [] + const pendingRequests = pendingRequestsData?.pendingRequests || [] + const activatedSubscriptions = activatedSubscriptionsData?.activatedSubscriptions || [] + + const handleActivatePlan = useCallback(async ({ priceId, isTrial = true, planName = '', trialDays = 0, isCustomPrice = false }: ActivatePlanParams) => { + if (!organization) return + + setActivateLoading(true) + try { + await activateSubscriptionPlan({ + variables: { + data: { + dv: 1, + sender: getClientSideSenderInfo(), + organization: { id: organization.id }, + pricingRule: { id: priceId }, + isTrial, + }, + }, + }) + + if (isTrial) { + await refetchTrialSubscriptions() + await refetchPendingRequests() + await refetchActivatedSubscriptions() + if (employee?.id) { + await selectEmployee(employee.id) + } + notification.success({ + message: ( + + {intl.formatMessage({ id: 'subscription.activation.trial.title' }, { planName })} + + ), + description: intl.formatMessage({ id: 'subscription.activation.trial.description' }, { planName, days: trialDays }), + duration: 5, + }) + } else { + await refetchPendingRequests() + if (isCustomPrice) { + notification.success({ + message: ( + + {intl.formatMessage({ id: 'subscription.activation.paid.custom.title' }, { planName })} + + ), + description: intl.formatMessage({ id: 'subscription.activation.paid.custom.description' }), + duration: 5, + }) + } else { + notification.success({ + message: ( + + {intl.formatMessage({ id: 'subscription.activation.paid.standard.title' })} + + ), + description: intl.formatMessage({ id: 'subscription.activation.paid.standard.description' }), + duration: 5, + }) + } + } + } catch (error) { + console.error('Failed to activate subscription:', error) + notification.error({ + message: ActivationErrorTitle, + description: error?.message || ActivationErrorMessage, + duration: 5, + }) + } finally { + setActivateLoading(false) + } + }, [organization, activateSubscriptionPlan, refetchTrialSubscriptions, refetchPendingRequests, refetchActivatedSubscriptions, employee?.id, intl, selectEmployee, ActivationErrorTitle, ActivationErrorMessage]) + + return { + handleActivatePlan, + activateLoading, + trialSubscriptions, + pendingRequests, + activatedSubscriptions, + isLoading: trialSubscriptionsLoading || pendingRequestsLoading || activatedSubscriptionsLoading, + } +} diff --git a/apps/condo/domains/ticket/components/BaseTicketForm/index.tsx b/apps/condo/domains/ticket/components/BaseTicketForm/index.tsx index 7f8ace28874..79bce2fc86f 100644 --- a/apps/condo/domains/ticket/components/BaseTicketForm/index.tsx +++ b/apps/condo/domains/ticket/components/BaseTicketForm/index.tsx @@ -66,7 +66,7 @@ import { MANAGING_COMPANY_TYPE, SERVICE_PROVIDER_TYPE } from '@condo/domains/org import { PropertyAddressSearchInput } from '@condo/domains/property/components/PropertyAddressSearchInput' import { UnitInfo, UnitInfoMode } from '@condo/domains/property/components/UnitInfo' import { PropertyFormItemTooltip } from '@condo/domains/property/PropertyFormItemTooltip' -import { NoSubscriptionTooltip } from '@condo/domains/subscription/components' +import { FeatureGate } from '@condo/domains/subscription/components' import { useOrganizationSubscription } from '@condo/domains/subscription/hooks' import { IncidentHints } from '@condo/domains/ticket/components/IncidentHints' import { useTicketThreeLevelsClassifierHook } from '@condo/domains/ticket/components/TicketClassifierSelect' @@ -216,29 +216,30 @@ const AddInvoiceButton = ({ initialValues, form, organizationId, ticketCreatedBy return null } - if (!hasMarketplaceFeature) { - return ( - -
- - - - {AddInvoiceMessage} - - -
-
- ) - } - return ( <> - setCreateInvoiceModalOpen(true)}> - - - {AddInvoiceMessage} - - + + + + + {AddInvoiceMessage} + + + + } + > + setCreateInvoiceModalOpen(true)}> + + + {AddInvoiceMessage} + + + { createInvoiceModalOpen && ( = ({ inci rewriteIncidentTextForResident: rewriteIncidentTextForResidentEnabled, } } = useAIConfig() - const { isFeatureAvailable } = useOrganizationSubscription() - const hasAiFeature = isFeatureAvailable('ai') useEffect(() => { setRewriteText(rewriteTextData?.result?.answer) @@ -480,40 +477,41 @@ export const TextForResidentInput: React.FC = ({ inci icon={copied ? () : () } />
, - ...( - aiEnabled && rewriteIncidentTextForResidentEnabled ? [ - hasAiFeature ? ( - - ) : ( - -
- -
-
- ), - ] : [] - ), + ... + aiEnabled && rewriteIncidentTextForResidentEnabled ? [ + + + + } + > + + , + ] : [], ]} /> diff --git a/apps/condo/domains/ticket/components/IncidentForm/CreateIncidentForm.tsx b/apps/condo/domains/ticket/components/IncidentForm/CreateIncidentForm.tsx index 388d6b6907b..ce29b9b9bbd 100644 --- a/apps/condo/domains/ticket/components/IncidentForm/CreateIncidentForm.tsx +++ b/apps/condo/domains/ticket/components/IncidentForm/CreateIncidentForm.tsx @@ -11,8 +11,7 @@ import { ActionBar, Button, Space, Switch, Typography } from '@open-condo/ui' import { useAIConfig } from '@condo/domains/ai/hooks/useAIFlow' import { LabeledField } from '@condo/domains/common/components/LabeledField' import { AnalyticalNewsSources } from '@condo/domains/news/constants/sources' -import { NoSubscriptionTooltip } from '@condo/domains/subscription/components' -import { useOrganizationSubscription } from '@condo/domains/subscription/hooks' +import { FeatureGate } from '@condo/domains/subscription/components' import { BaseIncidentForm, BaseIncidentFormProps } from './BaseIncidentForm' @@ -30,10 +29,6 @@ export const CreateIncidentActionBar: React.FC, ...((withNewsGeneration && aiEnabled && generateNewsByIncidentEnabled && canManageNewsItems) ? [ - hasRequiredFeatures ? ( - + + + + + + {GenerateNewsLabel} + + + + + } + > + - ) : ( - -
- - - - - {GenerateNewsLabel} - - - -
-
- ), +
, ] : [] ), diff --git a/apps/condo/domains/ticket/hooks/useIncidentUpdateStatusModal.tsx b/apps/condo/domains/ticket/hooks/useIncidentUpdateStatusModal.tsx index 2f76489f2a6..6196255041f 100644 --- a/apps/condo/domains/ticket/hooks/useIncidentUpdateStatusModal.tsx +++ b/apps/condo/domains/ticket/hooks/useIncidentUpdateStatusModal.tsx @@ -19,6 +19,7 @@ import { LabeledField } from '@condo/domains/common/components/LabeledField' import DatePicker from '@condo/domains/common/components/Pickers/DatePicker' import { useValidations } from '@condo/domains/common/hooks/useValidations' import { analytics } from '@condo/domains/common/utils/analytics' +import { FeatureGate } from '@condo/domains/subscription/components' import { handleChangeDate } from '@condo/domains/ticket/components/IncidentForm/BaseIncidentForm' import type { FormRule as Rule } from 'antd' @@ -173,26 +174,45 @@ export const useIncidentUpdateStatusModal: UseIncidentUpdateStatusModalType = ({ { (withNewsGeneration && aiEnabled && generateNewsByIncidentEnabled && canManageNewsItems && isActual) && ( - + + + + + {GenerateNewsSwitchLabel} + + + + + } > - - - - - - {GenerateNewsSwitchLabel} - - - + + + + + + + {GenerateNewsSwitchLabel} + + + + ) } - , + , ] : [], ]} /> diff --git a/apps/condo/domains/ticket/components/IncidentForm/CreateIncidentForm.tsx b/apps/condo/domains/ticket/components/IncidentForm/CreateIncidentForm.tsx index c354fd1b42f..2e8fccad3e9 100644 --- a/apps/condo/domains/ticket/components/IncidentForm/CreateIncidentForm.tsx +++ b/apps/condo/domains/ticket/components/IncidentForm/CreateIncidentForm.tsx @@ -11,7 +11,7 @@ import { ActionBar, Button, Space, Switch, Typography } from '@open-condo/ui' import { useAIConfig } from '@condo/domains/ai/hooks/useAIFlow' import { LabeledField } from '@condo/domains/common/components/LabeledField' import { AnalyticalNewsSources } from '@condo/domains/news/constants/sources' -import { FeatureGate } from '@condo/domains/subscription/components' +import { SubscriptionFeatureGuard } from '@condo/domains/subscription/components' import { BaseIncidentForm, BaseIncidentFormProps } from './BaseIncidentForm' @@ -43,7 +43,7 @@ export const CreateIncidentActionBar: React.FC, ...((withNewsGeneration && aiEnabled && generateNewsByIncidentEnabled && canManageNewsItems) ? [ -
- , + , ] : [] ), diff --git a/apps/condo/domains/ticket/hooks/useIncidentUpdateStatusModal.tsx b/apps/condo/domains/ticket/hooks/useIncidentUpdateStatusModal.tsx index 8d3a3e0df17..9fc77d6cf1a 100644 --- a/apps/condo/domains/ticket/hooks/useIncidentUpdateStatusModal.tsx +++ b/apps/condo/domains/ticket/hooks/useIncidentUpdateStatusModal.tsx @@ -19,7 +19,7 @@ import { LabeledField } from '@condo/domains/common/components/LabeledField' import DatePicker from '@condo/domains/common/components/Pickers/DatePicker' import { useValidations } from '@condo/domains/common/hooks/useValidations' import { analytics } from '@condo/domains/common/utils/analytics' -import { FeatureGate } from '@condo/domains/subscription/components' +import { SubscriptionFeatureGuard } from '@condo/domains/subscription/components' import { handleChangeDate } from '@condo/domains/ticket/components/IncidentForm/BaseIncidentForm' import type { FormRule as Rule } from 'antd' @@ -174,7 +174,7 @@ export const useIncidentUpdateStatusModal: UseIncidentUpdateStatusModalType = ({ { (withNewsGeneration && aiEnabled && generateNewsByIncidentEnabled && canManageNewsItems && isActual) && ( - - + ) } } + * > + * + * + */ +export const SubscriptionGuardWithTooltip: React.FC = ({ children, feature, fallback, diff --git a/apps/condo/domains/subscription/components/index.ts b/apps/condo/domains/subscription/components/index.ts index 15291b0a30f..a56d52b3266 100644 --- a/apps/condo/domains/subscription/components/index.ts +++ b/apps/condo/domains/subscription/components/index.ts @@ -1,6 +1,6 @@ export { NoSubscriptionTooltip } from './NoSubscriptionTooltip' export type { NoSubscriptionTooltipProps } from './NoSubscriptionTooltip' -export { SubscriptionFeatureGuard } from './SubscriptionFeatureGuard' +export { SubscriptionGuardWithTooltip } from './SubscriptionGuardWithTooltip' export { SubscriptionAccessGuard } from './SubscriptionAccessGuard' export { SubscriptionWelcomeModal } from './SubscriptionWelcomeModal' export { SubscriptionTrialEndedModal } from './SubscriptionTrialEndedModal' diff --git a/apps/condo/domains/ticket/components/BaseTicketForm/index.tsx b/apps/condo/domains/ticket/components/BaseTicketForm/index.tsx index 107daeab453..92e9de2555d 100644 --- a/apps/condo/domains/ticket/components/BaseTicketForm/index.tsx +++ b/apps/condo/domains/ticket/components/BaseTicketForm/index.tsx @@ -66,7 +66,7 @@ import { MANAGING_COMPANY_TYPE, SERVICE_PROVIDER_TYPE } from '@condo/domains/org import { PropertyAddressSearchInput } from '@condo/domains/property/components/PropertyAddressSearchInput' import { UnitInfo, UnitInfoMode } from '@condo/domains/property/components/UnitInfo' import { PropertyFormItemTooltip } from '@condo/domains/property/PropertyFormItemTooltip' -import { SubscriptionFeatureGuard } from '@condo/domains/subscription/components' +import { SubscriptionGuardWithTooltip } from '@condo/domains/subscription/components' import { useOrganizationSubscription } from '@condo/domains/subscription/hooks' import { IncidentHints } from '@condo/domains/ticket/components/IncidentHints' import { useTicketThreeLevelsClassifierHook } from '@condo/domains/ticket/components/TicketClassifierSelect' @@ -219,7 +219,7 @@ const AddInvoiceButton = ({ initialValues, form, organizationId, ticketCreatedBy return ( <> - {AddInvoiceMessage} - + { createInvoiceModalOpen && ( = ({ inci , ... aiEnabled && rewriteIncidentTextForResidentEnabled ? [ - = ({ inci > {UpdateTextMessage} - , + , ] : [], ]} /> diff --git a/apps/condo/domains/ticket/components/IncidentForm/CreateIncidentForm.tsx b/apps/condo/domains/ticket/components/IncidentForm/CreateIncidentForm.tsx index 2e8fccad3e9..e976b300da6 100644 --- a/apps/condo/domains/ticket/components/IncidentForm/CreateIncidentForm.tsx +++ b/apps/condo/domains/ticket/components/IncidentForm/CreateIncidentForm.tsx @@ -11,7 +11,7 @@ import { ActionBar, Button, Space, Switch, Typography } from '@open-condo/ui' import { useAIConfig } from '@condo/domains/ai/hooks/useAIFlow' import { LabeledField } from '@condo/domains/common/components/LabeledField' import { AnalyticalNewsSources } from '@condo/domains/news/constants/sources' -import { SubscriptionFeatureGuard } from '@condo/domains/subscription/components' +import { SubscriptionGuardWithTooltip } from '@condo/domains/subscription/components' import { BaseIncidentForm, BaseIncidentFormProps } from './BaseIncidentForm' @@ -43,7 +43,7 @@ export const CreateIncidentActionBar: React.FC, ...((withNewsGeneration && aiEnabled && generateNewsByIncidentEnabled && canManageNewsItems) ? [ - - , + , ] : [] ), diff --git a/apps/condo/domains/ticket/hooks/useIncidentUpdateStatusModal.tsx b/apps/condo/domains/ticket/hooks/useIncidentUpdateStatusModal.tsx index 4da8c32688d..0a1771560ff 100644 --- a/apps/condo/domains/ticket/hooks/useIncidentUpdateStatusModal.tsx +++ b/apps/condo/domains/ticket/hooks/useIncidentUpdateStatusModal.tsx @@ -19,7 +19,7 @@ import { LabeledField } from '@condo/domains/common/components/LabeledField' import DatePicker from '@condo/domains/common/components/Pickers/DatePicker' import { useValidations } from '@condo/domains/common/hooks/useValidations' import { analytics } from '@condo/domains/common/utils/analytics' -import { SubscriptionFeatureGuard } from '@condo/domains/subscription/components' +import { SubscriptionGuardWithTooltip } from '@condo/domains/subscription/components' import { handleChangeDate } from '@condo/domains/ticket/components/IncidentForm/BaseIncidentForm' import type { FormRule as Rule } from 'antd' @@ -174,7 +174,7 @@ export const useIncidentUpdateStatusModal: UseIncidentUpdateStatusModalType = ({ { (withNewsGeneration && aiEnabled && generateNewsByIncidentEnabled && canManageNewsItems && isActual) && ( - - + ) }