Skip to content

Commit

Permalink
feat: Show error messages from server on billing page (#21765)
Browse files Browse the repository at this point in the history
* show error messages on billing page

* some more updates

* extraneous exception and pr feedback

* Move error display into the unsubscribe modal
* Only show link to stripe if the error detail is about open invoices

* Update frontend/src/scenes/billing/billingLogic.tsx

Co-authored-by: Raquel Smith <[email protected]>

* remove opensupportform from billingLogic

---------

Co-authored-by: Raquel Smith <[email protected]>
  • Loading branch information
xrdt and raquelmsmith authored Apr 25, 2024
1 parent dd03cd6 commit f4fe463
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 28 deletions.
21 changes: 15 additions & 6 deletions ee/api/billing.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
from ee.settings import BILLING_SERVICE_URL
from posthog.api.routing import TeamAndOrgViewSetMixin
from posthog.cloud_utils import get_cached_instance_license
from posthog.models import Organization
from posthog.event_usage import groups
from posthog.models import Organization

logger = structlog.get_logger(__name__)

Expand Down Expand Up @@ -75,9 +75,9 @@ def patch(self, request: Request, *args: Any, **kwargs: Any) -> Response:
distinct_id,
"billing limits updated",
properties={**custom_limits_usd},
groups=groups(org, self.request.user.team)
if hasattr(self.request.user, "team")
else groups(org),
groups=(
groups(org, self.request.user.team) if hasattr(self.request.user, "team") else groups(org)
),
)
posthoganalytics.group_identify(
"organization",
Expand Down Expand Up @@ -125,8 +125,17 @@ def deactivate(self, request: Request, *args: Any, **kwargs: Any) -> HttpRespons
product = request.GET.get("products", None)
if not product:
raise ValidationError("Products must be specified")

BillingManager(license).deactivate_products(organization, product)
try:
BillingManager(license).deactivate_products(organization, product)
except Exception as e:
if len(e.args) > 2:
detail_object = e.args[2]
return Response(
{"statusText": e.args[0], "detail": detail_object.get("error_message", detail_object)},
status=status.HTTP_400_BAD_REQUEST,
)
else:
raise e
return self.list(request, *args, **kwargs)

@action(methods=["PATCH"], detail=False)
Expand Down
2 changes: 1 addition & 1 deletion ee/billing/billing_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def build_billing_token(license: License, organization: Organization):
def handle_billing_service_error(res: requests.Response, valid_codes=(200, 404, 401)) -> None:
if res.status_code not in valid_codes:
logger.error(f"Billing service returned bad status code: {res.status_code}, body: {res.text}")
raise Exception(f"Billing service returned bad status code: {res.status_code}, body: {res.text}")
raise Exception(f"Billing service returned bad status code: {res.status_code}", f"body:", res.json())


class BillingManager:
Expand Down
8 changes: 0 additions & 8 deletions frontend/src/scenes/billing/Billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { Field, Form } from 'kea-forms'
import { router } from 'kea-router'
import { BillingUpgradeCTA } from 'lib/components/BillingUpgradeCTA'
import { SurprisedHog } from 'lib/components/hedgehogs'
import { PageHeader } from 'lib/components/PageHeader'
import { supportLogic } from 'lib/components/Support/supportLogic'
import { FEATURE_FLAGS } from 'lib/constants'
import { dayjs } from 'lib/dayjs'
Expand All @@ -32,10 +31,6 @@ export const scene: SceneExport = {
logic: billingLogic,
}

export function BillingPageHeader(): JSX.Element {
return <PageHeader />
}

export function Billing(): JSX.Element {
const {
billing,
Expand Down Expand Up @@ -71,7 +66,6 @@ export function Billing(): JSX.Element {
if (!billing && billingLoading) {
return (
<>
<BillingPageHeader />
<SpinnerOverlay sceneLevel />
</>
)
Expand All @@ -80,7 +74,6 @@ export function Billing(): JSX.Element {
if (!billing && !billingLoading) {
return (
<div className="space-y-4">
{!isOnboarding && <BillingPageHeader />}
<LemonBanner type="error">
{
'There was an issue retrieving your current billing information. If this message persists, please '
Expand Down Expand Up @@ -138,7 +131,6 @@ export function Billing(): JSX.Element {

return (
<div ref={ref}>
{!isOnboarding && <BillingPageHeader />}
{showLicenseDirectInput && (
<>
<Form logic={billingLogic} formKey="activateLicense" enableFormOnSubmit className="space-y-4">
Expand Down
16 changes: 11 additions & 5 deletions frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ export const UnsubscribeSurveyModal = ({
product: BillingProductV2Type | BillingProductV2AddonType
}): JSX.Element | null => {
const { surveyID, surveyResponse } = useValues(billingProductLogic({ product }))
const { setSurveyResponse, reportSurveySent, reportSurveyDismissed } = useActions(billingProductLogic({ product }))
const { setSurveyResponse, reportSurveyDismissed } = useActions(billingProductLogic({ product }))
const { deactivateProduct } = useActions(billingLogic)
const { unsubscribeError, billingLoading } = useValues(billingLogic)
const { unsubscribeDisabledReason, itemsToDisable } = useValues(exportsUnsubscribeTableLogic)

const textAreaNotEmpty = surveyResponse['$survey_response']?.length > 0
Expand Down Expand Up @@ -45,18 +46,23 @@ export const UnsubscribeSurveyModal = ({
type={textAreaNotEmpty ? 'primary' : 'secondary'}
disabledReason={includesPipelinesAddon && unsubscribeDisabledReason}
onClick={() => {
textAreaNotEmpty
? reportSurveySent(surveyID, surveyResponse)
: reportSurveyDismissed(surveyID)
deactivateProduct(product.type)
}}
loading={billingLoading}
>
Unsubscribe
</LemonButton>
</>
}
>
<div className="flex flex-col gap-3.5">
{unsubscribeError && (
<LemonBanner type="error">
<p>
{unsubscribeError.detail} {unsubscribeError.link}
</p>
</LemonBanner>
)}
<LemonTextArea
data-attr="unsubscribe-reason-survey-textarea"
placeholder="Reason for unsubscribing..."
Expand Down Expand Up @@ -87,7 +93,6 @@ export const UnsubscribeSurveyModal = ({
>
chat with support
</Link>

{product.type === 'session_replay' && (
<>
{', or '}
Expand All @@ -103,6 +108,7 @@ export const UnsubscribeSurveyModal = ({
{' for tuning recording volume with sampling and minimum duration.'}
</>
)}
.
</p>
</LemonBanner>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { lemonToast } from '@posthog/lemon-ui'
import { lemonToast, Link } from '@posthog/lemon-ui'
import { actions, afterMount, connect, kea, listeners, path, reducers, selectors } from 'kea'
import { forms } from 'kea-forms'
import { loaders } from 'kea-loaders'
import { router, urlToAction } from 'kea-router'
import api from 'lib/api'
import api, { getJSONOrNull } from 'lib/api'
import { dayjs } from 'lib/dayjs'
import { LemonBannerAction } from 'lib/lemon-ui/LemonBanner/LemonBanner'
import { lemonBannerLogic } from 'lib/lemon-ui/LemonBanner/lemonBannerLogic'
Expand Down Expand Up @@ -33,6 +33,11 @@ export interface BillingAlertConfig {
onClose?: () => void
}

export interface UnsubscribeError {
detail: string | JSX.Element
link: JSX.Element
}

const parseBillingResponse = (data: Partial<BillingV2Type>): BillingV2Type => {
if (data.billing_period) {
data.billing_period = {
Expand Down Expand Up @@ -70,6 +75,8 @@ export const billingLogic = kea<billingLogicType>([
setRedirectPath: true,
setIsOnboarding: true,
determineBillingAlert: true,
setUnsubscribeError: (error: null | UnsubscribeError) => ({ error }),
resetUnsubscribeError: true,
setBillingAlert: (billingAlert: BillingAlertConfig | null) => ({ billingAlert }),
}),
connect(() => ({
Expand Down Expand Up @@ -126,8 +133,15 @@ export const billingLogic = kea<billingLogicType>([
setIsOnboarding: () => window.location.pathname.includes('/onboarding'),
},
],
unsubscribeError: [
null as null | UnsubscribeError,
{
resetUnsubscribeError: () => null,
setUnsubscribeError: (_, { error }) => error,
},
],
}),
loaders(({ actions }) => ({
loaders(({ actions, values }) => ({
billing: [
null as BillingV2Type | null,
{
Expand All @@ -145,10 +159,34 @@ export const billingLogic = kea<billingLogicType>([
},

deactivateProduct: async (key: string) => {
const response = await api.get('api/billing-v2/deactivate?products=' + key)
lemonToast.success('Product unsubscribed')
actions.reportProductUnsubscribed(key)
return parseBillingResponse(response)
actions.resetUnsubscribeError()
try {
const response = await api.getResponse('api/billing-v2/deactivate?products=' + key)
const jsonRes = await getJSONOrNull(response)
lemonToast.success('Product unsubscribed')
actions.reportProductUnsubscribed(key)
return parseBillingResponse(jsonRes)
} catch (error: any) {
if (error.detail && error.detail.includes('open invoice')) {
actions.setUnsubscribeError({
detail: error.detail,
link: (
<Link to={values.billing?.stripe_portal_url} target="_blank">
View invoices
</Link>
),
} as UnsubscribeError)
} else {
actions.setUnsubscribeError({
detail:
error.detail ||
`We encountered a problem. Please try again or submit a support ticket.`,
} as UnsubscribeError)
}
console.error(error)
// This is a bit of a hack to prevent the page from re-rendering.
return values.billing
}
},
},
],
Expand Down
11 changes: 10 additions & 1 deletion frontend/src/scenes/billing/billingProductLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const billingProductLogic = kea<billingProductLogicType>([
key((props) => props.product.type),
path(['scenes', 'billing', 'billingProductLogic']),
connect({
values: [billingLogic, ['billing', 'isUnlicensedDebug', 'scrollToProductKey']],
values: [billingLogic, ['billing', 'isUnlicensedDebug', 'scrollToProductKey', 'unsubscribeError']],
actions: [
billingLogic,
[
Expand All @@ -34,6 +34,7 @@ export const billingProductLogic = kea<billingProductLogicType>([
'deactivateProduct',
'setProductSpecificAlert',
'setScrollToProductKey',
'deactivateProductSuccess',
],
],
}),
Expand Down Expand Up @@ -251,6 +252,14 @@ export const billingProductLogic = kea<billingProductLogicType>([
})
actions.setSurveyID('')
},
deactivateProductSuccess: () => {
if (!values.unsubscribeError) {
const textAreaNotEmpty = values.surveyResponse['$survey_response']?.length > 0
textAreaNotEmpty
? actions.reportSurveySent(values.surveyID, values.surveyResponse)
: actions.reportSurveyDismissed(values.surveyID)
}
},
setScrollToProductKey: ({ scrollToProductKey }) => {
if (scrollToProductKey && scrollToProductKey === props.product.type) {
const { currentPlan } = values.currentAndUpgradePlans
Expand Down

0 comments on commit f4fe463

Please sign in to comment.