Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 5 additions & 13 deletions apps/condo/domains/miniapp/components/AppDescription/TopCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

import { useContainerSize } from '@condo/domains/common/hooks/useContainerSize'
import { CONTEXT_IN_PROGRESS_STATUS } from '@condo/domains/miniapp/constants'
import { NoSubscriptionTooltip } from '@condo/domains/subscription/components'
import { SubscriptionGuardWithTooltip } from '@condo/domains/subscription/components'
import { useOrganizationSubscription } from '@condo/domains/subscription/hooks'

import { AppLabelTag } from '../AppLabelTag'
Expand Down Expand Up @@ -135,12 +135,6 @@

const buttonProps = useMemo<ButtonProps>(() => {
const btnProps: ButtonProps = { type: 'primary' }

if (!isAppAvailableForTariff) {
btnProps.children = UnavailableForTariffMessage
btnProps.disabled = true
return btnProps
}

if (!contextStatus) {
btnProps.children = intl.formatMessage({ id: 'miniapps.addDescription.action.connect' })
Expand Down Expand Up @@ -229,13 +223,11 @@
</Typography.Title>
)}
</Space>
{!isAppAvailableForTariff ? (
<NoSubscriptionTooltip>
<span><Button {...buttonProps}/></span>
</NoSubscriptionTooltip>
) : (
<SubscriptionGuardWithTooltip b2bAppId={id} fallback={
<span><Button {...buttonProps} disabled children={UnavailableForTariffMessage}/></span>

Check warning on line 227 in apps/condo/domains/miniapp/components/AppDescription/TopCard.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not pass children as props. Instead, nest children between the opening and closing tags.

See more on https://sonarcloud.io/project/issues?id=open-condo-software_condo&issues=AZzWfiQ-PaOt1ePD41mx&open=AZzWfiQ-PaOt1ePD41mx&pullRequest=7330
}>
<Button {...buttonProps}/>
)}
</SubscriptionGuardWithTooltip>
Comment on lines +226 to +230
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid passing children using the children prop; use JSX children syntax instead.

The static analysis tool flagged this as an error. Using the children prop directly is discouraged—prefer the standard JSX children pattern for clarity and to avoid potential issues.

🐛 Proposed fix
                     <SubscriptionGuardWithTooltip b2bAppId={id} fallback={
-                        <span><Button {...buttonProps} disabled children={UnavailableForTariffMessage}/></span>
+                        <span><Button {...buttonProps} disabled>{UnavailableForTariffMessage}</Button></span>
                     }>
                         <Button {...buttonProps}/>
                     </SubscriptionGuardWithTooltip>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<SubscriptionGuardWithTooltip b2bAppId={id} fallback={
<span><Button {...buttonProps} disabled children={UnavailableForTariffMessage}/></span>
}>
<Button {...buttonProps}/>
)}
</SubscriptionGuardWithTooltip>
<SubscriptionGuardWithTooltip b2bAppId={id} fallback={
<span><Button {...buttonProps} disabled>{UnavailableForTariffMessage}</Button></span>
}>
<Button {...buttonProps}/>
</SubscriptionGuardWithTooltip>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/condo/domains/miniapp/components/AppDescription/TopCard.tsx` around
lines 226 - 230, The fallback passed into SubscriptionGuardWithTooltip uses the
children prop on Button (children={UnavailableForTariffMessage}) which static
analysis flags; change the fallback to use JSX children syntax instead by
rendering Button with the disabled prop and placing UnavailableForTariffMessage
between its opening and closing tags (keep the surrounding span if needed), and
ensure the primary Button inside SubscriptionGuardWithTooltip remains unchanged;
update references to SubscriptionGuardWithTooltip, Button,
UnavailableForTariffMessage, buttonProps and id accordingly.

</Space>
</Col>
<Col span={sectionSpan} style={VERT_ALIGN_STYLES} ref={setCarouselColRef}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,20 @@
feature?: AvailableFeature | AvailableFeature[]
path?: string
skipTooltip?: boolean
b2bAppId?: string
}

export const NoSubscriptionTooltip: React.FC<NoSubscriptionTooltipProps> = ({ children, placement = 'right', feature: featureProp, path, skipTooltip }) => {
export const NoSubscriptionTooltip: React.FC<NoSubscriptionTooltipProps> = ({ children, placement = 'right', feature: featureProp, path, skipTooltip, b2bAppId }) => {
const intl = useIntl()
const router = useRouter()
const { organization } = useOrganization()
const { trialSubscriptions, activatedSubscriptions, handleActivatePlan, activateLoading } = useActivateSubscriptions()
const { isFeatureAvailable, hasSubscription } = useOrganizationSubscription()
const { isFeatureAvailable, hasSubscription, isB2BAppEnabled } = useOrganizationSubscription()
const [isActivating, setIsActivating] = useState(false)

const requiredFeature = path ? getRequiredFeature(path) : null
const feature = (featureProp || requiredFeature) as AvailableFeature | undefined | null
const isAppAvailableForTariff = b2bAppId ? isB2BAppEnabled(b2bAppId) : true

const FeatureLockedMessage = intl.formatMessage({
id: 'subscription.warns.noActiveSubscription',
Expand All @@ -50,14 +52,23 @@
})

const hasActivatedAnyTrial = trialSubscriptions.length > 0
const isAvailable = feature
? Array.isArray(feature)
? feature.every(f => isFeatureAvailable(f))
: isFeatureAvailable(feature)
: false

const isAvailable = useMemo(() => {
if (b2bAppId) {
return isAppAvailableForTariff
} else if (feature) {
if (Array.isArray(feature)) {
return feature.every(isFeatureAvailable)

Check failure on line 61 in apps/condo/domains/subscription/components/NoSubscriptionTooltip.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not pass function `isFeatureAvailable` directly to `.every(…)`.

See more on https://sonarcloud.io/project/issues?id=open-condo-software_condo&issues=AZzXFP8JPgyi_h8MLVSh&open=AZzXFP8JPgyi_h8MLVSh&pullRequest=7330
} else {
return isFeatureAvailable(feature)
}
} else {
return false
}
}, [b2bAppId, isAppAvailableForTariff, feature, isFeatureAvailable])
Comment on lines +56 to +68
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent behavior when neither feature nor b2bAppId is provided.

In NoSubscriptionTooltip, when neither is provided, isAvailable returns false (line 66), so the tooltip is shown.

However, in SubscriptionGuardWithTooltip, when neither is provided, isAppAvailableForTariff defaults to true (line 51), so children are rendered.

This inconsistency could lead to confusing behavior where the same scenario produces different outcomes depending on which component is used directly.

Also applies to: 51-59

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/condo/domains/subscription/components/NoSubscriptionTooltip.tsx` around
lines 56 - 68, NoSubscriptionTooltip and SubscriptionGuardWithTooltip have
mismatched defaults when neither b2bAppId nor feature is provided; make their
availability logic consistent. Update NoSubscriptionTooltip's isAvailable (in
the useMemo) to treat the "neither provided" case the same as
SubscriptionGuardWithTooltip (i.e., return isAppAvailableForTariff/default true)
or alternatively change SubscriptionGuardWithTooltip's isAppAvailableForTariff
default to false so both components behave the same; pick one approach and apply
it to both places (referencing NoSubscriptionTooltip::isAvailable,
SubscriptionGuardWithTooltip::isAppAvailableForTariff and the b2bAppId/feature
checks) so the same input state yields the same availability result across
components.


const bestPlanWithFeature = useMemo(() => {
if (!feature || isAvailable) return null
if ((!feature && !b2bAppId) || isAvailable) return null

const plans = plansData?.result?.plans || []

Expand All @@ -66,9 +77,17 @@
const plan = planInfo?.plan
if (!plan) return false

const hasFeature = Array.isArray(feature)
? feature.every(f => plan[f] === true)
: plan[feature] === true
let hasRequirement = false

if (b2bAppId) {
const enabledApps = plan.enabledB2BApps || []
hasRequirement = enabledApps.includes(b2bAppId)
} else if (feature) {
hasRequirement = 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
Expand All @@ -80,12 +99,12 @@
)
)

return hasFeature && hasTrialDays && hasPrice && !alreadyActivated
return hasRequirement && hasTrialDays && hasPrice && !alreadyActivated
})
.sort((a, b) => (b.plan?.priority ?? 0) - (a.plan?.priority ?? 0))

return plansWithFeature[0] || null
}, [feature, isAvailable, plansData?.result?.plans, activatedSubscriptions])
}, [feature, b2bAppId, isAvailable, plansData?.result?.plans, activatedSubscriptions])

const TryTrialButton = useMemo(() => {
const currencyCode = bestPlanWithFeature?.prices?.[0]?.currencyCode
Expand Down Expand Up @@ -127,11 +146,11 @@

const buttonText = hasActivatedAnyTrial ? ViewPlansButton : TryTrialButton

const buttonAction = !feature || hasActivatedAnyTrial ? handleViewPlans : handleActivateTrial
const buttonAction = (!feature && !b2bAppId) || hasActivatedAnyTrial ? handleViewPlans : handleActivateTrial

const tooltipTitle = (
<Space size={12} direction='vertical'>
<Typography.Text size='small'>{hasActivatedAnyTrial ? UpgradePlanMessage : FeatureLockedMessage}</Typography.Text>
<Typography.Text size='small'>{hasActivatedAnyTrial && !isLoading ? UpgradePlanMessage : FeatureLockedMessage}</Typography.Text>
Comment on lines +149 to +153
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use the "view plans" CTA when no matching trial exists.

After the new b2bAppId filtering, bestPlanWithFeature can be null even when hasActivatedAnyTrial is false (for example, no trial-enabled plan includes this miniapp, or all matching plans are already activated). In that state the tooltip still renders the trial CTA and only redirects to settings inside handleActivateTrial, so the copy promises a free trial that does not exist. Please derive the button text/action from bestPlanWithFeature as well.

💡 Proposed fix
-    const buttonText = hasActivatedAnyTrial ? ViewPlansButton : TryTrialButton
-    
-    const buttonAction = (!feature && !b2bAppId) || hasActivatedAnyTrial ? handleViewPlans : handleActivateTrial
+    const canStartTrial = Boolean(bestPlanWithFeature) && !hasActivatedAnyTrial
+    const buttonText = canStartTrial ? TryTrialButton : ViewPlansButton
+    
+    const buttonAction = canStartTrial ? handleActivateTrial : handleViewPlans
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/condo/domains/subscription/components/NoSubscriptionTooltip.tsx` around
lines 149 - 153, The tooltip currently chooses the trial CTA based only on
hasActivatedAnyTrial, which can be false while bestPlanWithFeature is null after
b2bAppId filtering; update the decision logic for both buttonAction and button
text so they also consider bestPlanWithFeature. Concretely, change the condition
that assigns buttonAction (and the logic that selects the CTA label used in
tooltipTitle) to fall back to handleViewPlans whenever bestPlanWithFeature is
null (i.e., use handleActivateTrial only when bestPlanWithFeature exists and
hasActivatedAnyTrial is false), referencing buttonAction, bestPlanWithFeature,
hasActivatedAnyTrial, handleViewPlans and handleActivateTrial to locate and
update the expressions.

<Button
type='accent'
size='medium'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,61 @@

interface SubscriptionFeatureGuardProps extends Omit<NoSubscriptionTooltipProps, 'children'> {
children: React.ReactElement
feature: NoSubscriptionTooltipProps['feature']
feature?: NoSubscriptionTooltipProps['feature']
fallback: React.ReactElement
}

/**
* Component that conditionally renders content based on subscription feature availability.
*
* If the organization has access to the specified feature(s), renders the children.
* Component that conditionally renders content based on subscription feature or miniapp availability.
*
* If the organization has access to the specified feature(s) or miniapp, renders the children.
* Otherwise, renders the fallback element wrapped in a NoSubscriptionTooltip.
*
* @param children - Element to render when feature is available
* @param feature - Feature name or array of feature names to check
* @param fallback - Element to render when feature is not available (will be wrapped in tooltip)
*
* @param children - Element to render when feature/miniapp is available
* @param feature - Feature name or array of feature names to check (optional if b2bAppId is provided)
* @param b2bAppId - Miniapp ID to check availability for (optional if feature is provided)
* @param fallback - Element to render when feature/miniapp is not available (will be wrapped in tooltip)
* @param tooltipProps - Additional props passed to NoSubscriptionTooltip
*
*
* @example
* // Feature-based guard
* <SubscriptionGuardWithTooltip
* feature="analytics"
* fallback={<Button disabled>Analytics</Button>}
* >
* <Button>Analytics</Button>
* </SubscriptionGuardWithTooltip>
*
* @example
* // Miniapp-based guard
* <SubscriptionGuardWithTooltip
* b2bAppId="miniapp-id"
* fallback={<Button disabled>Connect</Button>}
* >
* <Button>Connect</Button>
* </SubscriptionGuardWithTooltip>
*/
export const SubscriptionGuardWithTooltip: React.FC<SubscriptionFeatureGuardProps> = ({
children,
feature,
fallback,
b2bAppId,
...tooltipProps
}) => {
const { isFeatureAvailable } = useOrganizationSubscription()
const { isFeatureAvailable, isB2BAppEnabled } = useOrganizationSubscription()

const isAppAvailableForTariff = b2bAppId ? isB2BAppEnabled(b2bAppId) : true

const hasFeature = Array.isArray(feature)
? feature.every(f => isFeatureAvailable(f))
? feature.every(isFeatureAvailable)

Check failure on line 54 in apps/condo/domains/subscription/components/SubscriptionGuardWithTooltip.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not pass function `isFeatureAvailable` directly to `.every(…)`.

See more on https://sonarcloud.io/project/issues?id=open-condo-software_condo&issues=AZzXFQCjPgyi_h8MLVSi&open=AZzXFQCjPgyi_h8MLVSi&pullRequest=7330
: isFeatureAvailable(feature)
Comment on lines 53 to 55
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don't call isFeatureAvailable with undefined.

apps/condo/domains/miniapp/components/AppDescription/TopCard.tsx:226-230 already uses this component with only b2bAppId, so Line 55 currently evaluates isFeatureAvailable(undefined) on every render. useOrganizationSubscription.ts:66-79 happens to turn that into false, but this guard is relying on an invalid input shape instead of explicitly short-circuiting the feature branch.

💡 Proposed fix
-    const hasFeature = Array.isArray(feature)
-        ? feature.every(f => isFeatureAvailable(f))
-        : isFeatureAvailable(feature)
+    const hasFeature = feature
+        ? Array.isArray(feature)
+            ? feature.every(f => isFeatureAvailable(f))
+            : isFeatureAvailable(feature)
+        : false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const hasFeature = Array.isArray(feature)
? feature.every(f => isFeatureAvailable(f))
: isFeatureAvailable(feature)
const hasFeature = feature
? Array.isArray(feature)
? feature.every(f => isFeatureAvailable(f))
: isFeatureAvailable(feature)
: false
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/condo/domains/subscription/components/SubscriptionGuardWithTooltip.tsx`
around lines 53 - 55, The current hasFeature computation calls
isFeatureAvailable(undefined) when the prop feature is omitted; change it to
explicitly short-circuit when feature is null/undefined so we don't call
isFeatureAvailable with invalid input. Update the hasFeature line in
SubscriptionGuardWithTooltip (the hasFeature constant) to first check feature
=== undefined || feature === null and return true (i.e., do not block) otherwise
evaluate Array.isArray(feature) ? feature.every(f => isFeatureAvailable(f)) :
isFeatureAvailable(feature); keep references to hasFeature and
isFeatureAvailable intact so callers like TopCard (which passes only b2bAppId)
won't trigger isFeatureAvailable(undefined).


if (hasFeature) {
if (hasFeature || isAppAvailableForTariff) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Require feature access when no b2bAppId is provided

This condition now lets SubscriptionGuardWithTooltip render children for all feature-only usages because isAppAvailableForTariff defaults to true when b2bAppId is missing, so hasFeature || isAppAvailableForTariff is always truthy. As a result, existing callers that pass only feature (there are many across incident/comment/ticket flows) no longer show the fallback/tooltip and effectively lose UI subscription gating.

Useful? React with 👍 / 👎.

return children
}

return (
<NoSubscriptionTooltip feature={feature} {...tooltipProps}>
<NoSubscriptionTooltip feature={feature} b2bAppId={b2bAppId} {...tooltipProps}>
{fallback}
</NoSubscriptionTooltip>
)
Expand Down
17 changes: 15 additions & 2 deletions apps/condo/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@

const { isAuthenticated, isLoading } = useAuth()
const { employee, organization } = useOrganization()
const { hasSubscription } = useOrganizationSubscription()
const { hasSubscription, isB2BAppEnabled } = useOrganizationSubscription()
const disabled = !employee || !hasSubscription
const { isCollapsed } = useLayoutContext()
const { wrapElementIntoNoOrganizationToolTip } = useNoOrganizationToolTip()
Expand Down Expand Up @@ -403,6 +403,19 @@
// not a ReDoS issue: running on end user browser
// nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp
const miniAppsPattern = new RegExp(`/miniapps/${app.id}/.+`)
const isAppAvailable = isB2BAppEnabled(app.id)

const miniappTooltip = ({ element, placement }) => (
<NoSubscriptionTooltip b2bAppId={app.id} children={element} placement={placement} />

Check warning on line 409 in apps/condo/pages/_app.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not pass children as props. Instead, nest children between the opening and closing tags.

See more on https://sonarcloud.io/project/issues?id=open-condo-software_condo&issues=AZzWfiRoPaOt1ePD41my&open=AZzWfiRoPaOt1ePD41my&pullRequest=7330
)
Comment on lines +408 to +410
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid passing children using the children prop; use JSX children syntax instead.

The static analysis tool flagged this pattern. Use standard JSX children syntax for clarity.

🐛 Proposed fix
                         const miniappTooltip = ({ element, placement }) => (
-                            <NoSubscriptionTooltip b2bAppId={app.id} children={element} placement={placement} />
+                            <NoSubscriptionTooltip b2bAppId={app.id} placement={placement}>
+                                {element}
+                            </NoSubscriptionTooltip>
                         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const miniappTooltip = ({ element, placement }) => (
<NoSubscriptionTooltip b2bAppId={app.id} children={element} placement={placement} />
)
const miniappTooltip = ({ element, placement }) => (
<NoSubscriptionTooltip b2bAppId={app.id} placement={placement}>
{element}
</NoSubscriptionTooltip>
)
🧰 Tools
🪛 Biome (2.4.6)

[error] 409-409: Avoid passing children using a prop

(lint/correctness/noChildrenProp)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/condo/pages/_app.tsx` around lines 408 - 410, The miniappTooltip arrow
function currently passes children via the children prop to
NoSubscriptionTooltip; change it to use JSX children syntax instead: return
<NoSubscriptionTooltip b2bAppId={app.id}
placement={placement}>{element}</NoSubscriptionTooltip> from the miniappTooltip
function so NoSubscriptionTooltip receives its child element as standard JSX
children (keep the same props b2bAppId and placement and the parameter names
element/placement).


let tooltipDecorator = null
if (disabled) {
tooltipDecorator = wrapElementIntoNoOrganizationToolTip
} else if (!isAppAvailable) {
tooltipDecorator = miniappTooltip
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tooltip only for miniapps?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, because we use it in appsByCategories

}

return <MenuItem
id={`menu-item-app-${app.id}`}
key={`menu-item-app-${app.id}`}
Expand All @@ -412,7 +425,7 @@
labelRaw
disabled={disabled}
isCollapsed={isCollapsed}
toolTipDecorator={disabled ? wrapElementIntoNoOrganizationToolTip : null}
toolTipDecorator={tooltipDecorator}
excludePaths={[miniAppsPattern]}
/>
})}
Expand Down
Loading