diff --git a/frontend/src/lib/components/BillingAlertsV2.tsx b/frontend/src/lib/components/BillingAlertsV2.tsx index 21a4023a6262a..6b3a0a7b4c3bb 100644 --- a/frontend/src/lib/components/BillingAlertsV2.tsx +++ b/frontend/src/lib/components/BillingAlertsV2.tsx @@ -12,18 +12,26 @@ export function BillingAlertsV2(): JSX.Element | null { const [alertHidden, setAlertHidden] = useState(false) useEffect(() => { + if (billingAlert?.pathName && currentLocation.pathname !== billingAlert?.pathName) { + setAlertHidden(true) + } else { + setAlertHidden(false) + } if (billingAlert) { reportBillingAlertShown(billingAlert) } - }, [billingAlert]) + }, [billingAlert, currentLocation]) if (!billingAlert || alertHidden) { return null } - const showButton = billingAlert.contactSupport || currentLocation.pathname !== urls.organizationBilling() + const showButton = + billingAlert.action || billingAlert.contactSupport || currentLocation.pathname !== urls.organizationBilling() - const buttonProps = billingAlert.contactSupport + const buttonProps = billingAlert.action + ? billingAlert.action + : billingAlert.contactSupport ? { to: 'mailto:sales@posthog.com', children: billingAlert.buttonCTA || 'Contact support', diff --git a/frontend/src/scenes/billing/BillingLimitInput.tsx b/frontend/src/scenes/billing/BillingLimitInput.tsx index 6f1dd652a4acb..fac3943214ea7 100644 --- a/frontend/src/scenes/billing/BillingLimitInput.tsx +++ b/frontend/src/scenes/billing/BillingLimitInput.tsx @@ -7,12 +7,14 @@ import { billingProductLogic } from './billingProductLogic' import { LemonButton, LemonInput } from '@posthog/lemon-ui' import { Tooltip } from 'lib/lemon-ui/Tooltip' import clsx from 'clsx' +import { useRef } from 'react' export const BillingLimitInput = ({ product }: { product: BillingProductV2Type }): JSX.Element | null => { + const limitInputRef = useRef(null) const { billing, billingLoading } = useValues(billingLogic) const { updateBillingLimits } = useActions(billingLogic) const { isEditingBillingLimit, showBillingLimitInput, billingLimitInput, customLimitUsd } = useValues( - billingProductLogic({ product }) + billingProductLogic({ product, billingLimitInputRef: limitInputRef }) ) const { setIsEditingBillingLimit, setBillingLimitInput } = useActions(billingProductLogic({ product })) @@ -78,7 +80,7 @@ export const BillingLimitInput = ({ product }: { product: BillingProductV2Type } return null } return ( -
+
{!isEditingBillingLimit ? ( @@ -104,6 +106,7 @@ export const BillingLimitInput = ({ product }: { product: BillingProductV2Type } <>
): BillingV2Type => { @@ -53,6 +56,8 @@ const parseBillingResponse = (data: Partial): BillingV2Type => { export const billingLogic = kea([ path(['scenes', 'billing', 'billingLogic']), actions({ + setProductSpecificAlert: (productSpecificAlert: BillingAlertConfig | null) => ({ productSpecificAlert }), + setScrollToProductKey: (scrollToProductKey: ProductKey | null) => ({ scrollToProductKey }), setShowLicenseDirectInput: (show: boolean) => ({ show }), reportBillingAlertShown: (alertConfig: BillingAlertConfig) => ({ alertConfig }), reportBillingAlertActionClicked: (alertConfig: BillingAlertConfig) => ({ alertConfig }), @@ -66,6 +71,18 @@ export const billingLogic = kea([ actions: [userLogic, ['loadUser'], eventUsageLogic, ['reportProductUnsubscribed']], }), reducers({ + scrollToProductKey: [ + null as ProductKey | null, + { + setScrollToProductKey: (_, { scrollToProductKey }) => scrollToProductKey, + }, + ], + productSpecificAlert: [ + null as BillingAlertConfig | null, + { + setProductSpecificAlert: (_, { productSpecificAlert }) => productSpecificAlert, + }, + ], showLicenseDirectInput: [ false, { @@ -144,8 +161,12 @@ export const billingLogic = kea([ }, ], billingAlert: [ - (s) => [s.billing, s.preflight, s.projectedTotalAmountUsd], - (billing, preflight, projectedTotalAmountUsd): BillingAlertConfig | undefined => { + (s) => [s.billing, s.preflight, s.projectedTotalAmountUsd, s.productSpecificAlert], + (billing, preflight, projectedTotalAmountUsd, productSpecificAlert): BillingAlertConfig | undefined => { + if (productSpecificAlert) { + return productSpecificAlert + } + if (!billing || !preflight?.cloud) { return } @@ -320,6 +341,10 @@ export const billingLogic = kea([ actions.setActivateLicenseValues({ license: hash.license }) actions.submitActivateLicense() } + if (_search.products) { + const products = _search.products.split(',') + actions.setScrollToProductKey(products[0]) + } actions.setRedirectPath() actions.setIsOnboarding() }, diff --git a/frontend/src/scenes/billing/billingProductLogic.ts b/frontend/src/scenes/billing/billingProductLogic.ts index aeb72f177c5be..723e152e9723d 100644 --- a/frontend/src/scenes/billing/billingProductLogic.ts +++ b/frontend/src/scenes/billing/billingProductLogic.ts @@ -1,21 +1,34 @@ -import { actions, connect, kea, key, listeners, path, props, reducers, selectors } from 'kea' +import { actions, connect, events, kea, key, listeners, path, props, reducers, selectors } from 'kea' import { BillingProductV2AddonType, BillingProductV2Type, BillingV2PlanType, BillingV2TierType } from '~/types' import { billingLogic } from './billingLogic' import type { billingProductLogicType } from './billingProductLogicType' import { convertAmountToUsage } from './billing-utils' import posthog from 'posthog-js' +import React from 'react' const DEFAULT_BILLING_LIMIT = 500 +export interface BillingProductLogicProps { + product: BillingProductV2Type | BillingProductV2AddonType + billingLimitInputRef?: React.MutableRefObject +} + export const billingProductLogic = kea([ + props({} as BillingProductLogicProps), key((props) => props.product.type), path(['scenes', 'billing', 'billingProductLogic']), connect({ - values: [billingLogic, ['billing', 'isUnlicensedDebug']], - actions: [billingLogic, ['loadBillingSuccess', 'updateBillingLimitsSuccess', 'deactivateProduct']], - }), - props({ - product: {} as BillingProductV2Type | BillingProductV2AddonType, + values: [billingLogic, ['billing', 'isUnlicensedDebug', 'scrollToProductKey']], + actions: [ + billingLogic, + [ + 'loadBillingSuccess', + 'updateBillingLimitsSuccess', + 'deactivateProduct', + 'setProductSpecificAlert', + 'setScrollToProductKey', + ], + ], }), actions({ setIsEditingBillingLimit: (isEditingBillingLimit: boolean) => ({ isEditingBillingLimit }), @@ -215,5 +228,40 @@ export const billingProductLogic = kea([ }) actions.setSurveyID('') }, + setScrollToProductKey: ({ scrollToProductKey }) => { + if (scrollToProductKey && scrollToProductKey === props.product.type) { + const { currentPlan } = values.currentAndUpgradePlans + + if (currentPlan.initial_billing_limit) { + actions.setProductSpecificAlert({ + status: 'warning', + title: 'Billing Limit Automatically Applied', + pathName: '/organization/billing', + dismissKey: `auto-apply-billing-limit-${props.product.type}`, + message: `To protect your costs and ours, we've automatically applied a $${currentPlan?.initial_billing_limit} billing limit for ${props.product.name}.`, + action: { + onClick: () => { + actions.setIsEditingBillingLimit(true) + setTimeout(() => { + if (props.billingLimitInputRef?.current) { + props.billingLimitInputRef?.current.focus() + props.billingLimitInputRef?.current.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + }) + } + }, 0) + }, + children: 'Update billing limit', + }, + }) + } + } + }, + })), + events(({ actions, values }) => ({ + afterMount: () => { + actions.setScrollToProductKey(values.scrollToProductKey) + }, })), ])