Skip to content

Commit

Permalink
feat: show alert when we automatically apply billing limit upon subsc…
Browse files Browse the repository at this point in the history
…ribe (#18668)

* show alert when we automatically apply billing limit upon subscribe

* Update to use billingAlert banner

* typing

* change banner to warning type

* address pr feedback

* add conditional for initial_billing_limit

* only show billing product alert on organization/billing page

---------

Co-authored-by: Bianca Yang <[email protected]>
  • Loading branch information
xrdt and Bianca Yang authored Nov 21, 2023
1 parent c9724a4 commit 491c783
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 14 deletions.
14 changes: 11 additions & 3 deletions frontend/src/lib/components/BillingAlertsV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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:[email protected]',
children: billingAlert.buttonCTA || 'Contact support',
Expand Down
7 changes: 5 additions & 2 deletions frontend/src/scenes/billing/BillingLimitInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLInputElement | null>(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 }))

Expand Down Expand Up @@ -78,7 +80,7 @@ export const BillingLimitInput = ({ product }: { product: BillingProductV2Type }
return null
}
return (
<div className="border-t border-border p-8">
<div className="border-t border-border p-8" data-attr={`billing-limit-input-${product.type}`}>
<div className="flex">
<div className="flex items-center gap-1">
{!isEditingBillingLimit ? (
Expand All @@ -104,6 +106,7 @@ export const BillingLimitInput = ({ product }: { product: BillingProductV2Type }
<>
<div className="max-w-40">
<LemonInput
ref={limitInputRef}
type="number"
fullWidth={false}
value={billingLimitInput}
Expand Down
31 changes: 28 additions & 3 deletions frontend/src/scenes/billing/billingLogic.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { kea, path, actions, connect, afterMount, selectors, listeners, reducers } from 'kea'
import { loaders } from 'kea-loaders'
import api from 'lib/api'
import { BillingProductV2Type, BillingV2Type } from '~/types'
import { BillingProductV2Type, BillingV2Type, ProductKey } from '~/types'
import { router, urlToAction } from 'kea-router'
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
import { dayjs } from 'lib/dayjs'
Expand All @@ -13,6 +13,7 @@ import { pluralize } from 'lib/utils'
import type { billingLogicType } from './billingLogicType'
import { forms } from 'kea-forms'
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
import { LemonBannerAction } from 'lib/lemon-ui/LemonBanner/LemonBanner'

export const ALLOCATION_THRESHOLD_ALERT = 0.85 // Threshold to show warning of event usage near limit
export const ALLOCATION_THRESHOLD_BLOCK = 1.2 // Threshold to block usage
Expand All @@ -24,6 +25,8 @@ export interface BillingAlertConfig {
contactSupport?: boolean
buttonCTA?: string
dismissKey?: string
action?: LemonBannerAction
pathName?: string
}

const parseBillingResponse = (data: Partial<BillingV2Type>): BillingV2Type => {
Expand Down Expand Up @@ -53,6 +56,8 @@ const parseBillingResponse = (data: Partial<BillingV2Type>): BillingV2Type => {
export const billingLogic = kea<billingLogicType>([
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 }),
Expand All @@ -66,6 +71,18 @@ export const billingLogic = kea<billingLogicType>([
actions: [userLogic, ['loadUser'], eventUsageLogic, ['reportProductUnsubscribed']],
}),
reducers({
scrollToProductKey: [
null as ProductKey | null,
{
setScrollToProductKey: (_, { scrollToProductKey }) => scrollToProductKey,
},
],
productSpecificAlert: [
null as BillingAlertConfig | null,
{
setProductSpecificAlert: (_, { productSpecificAlert }) => productSpecificAlert,
},
],
showLicenseDirectInput: [
false,
{
Expand Down Expand Up @@ -144,8 +161,12 @@ export const billingLogic = kea<billingLogicType>([
},
],
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
}
Expand Down Expand Up @@ -320,6 +341,10 @@ export const billingLogic = kea<billingLogicType>([
actions.setActivateLicenseValues({ license: hash.license })
actions.submitActivateLicense()
}
if (_search.products) {
const products = _search.products.split(',')
actions.setScrollToProductKey(products[0])
}
actions.setRedirectPath()
actions.setIsOnboarding()
},
Expand Down
60 changes: 54 additions & 6 deletions frontend/src/scenes/billing/billingProductLogic.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement | null>
}

export const billingProductLogic = kea<billingProductLogicType>([
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 }),
Expand Down Expand Up @@ -215,5 +228,40 @@ export const billingProductLogic = kea<billingProductLogicType>([
})
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)
},
})),
])

0 comments on commit 491c783

Please sign in to comment.