Skip to content

Commit

Permalink
feat: add reasons to unsub modal (PostHog#24109)
Browse files Browse the repository at this point in the history
  • Loading branch information
zlwaterfield authored and silentninja committed Aug 8, 2024
1 parent 9daacb1 commit e98701b
Show file tree
Hide file tree
Showing 13 changed files with 91 additions and 35 deletions.
9 changes: 6 additions & 3 deletions cypress/e2e/billing.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ describe('Billing', () => {

cy.get('[data-attr=more-button]').first().click()
cy.contains('.LemonButton', 'Unsubscribe').click()
cy.get('.LemonModal h3').should('contain', 'Why are you unsubscribing from Product analytics?')
cy.get('.LemonModal h3').should('contain', 'Unsubscribe from Product analytics')
cy.get('[data-attr=unsubscribe-reason-too-expensive]').click()
cy.get('[data-attr=unsubscribe-reason-survey-textarea]').type('Product analytics')
cy.contains('.LemonModal .LemonButton', 'Unsubscribe').click()

Expand All @@ -26,6 +27,8 @@ describe('Billing', () => {
expect(matchingEvent.properties.$survey_id).to.equal(UNSUBSCRIBE_SURVEY_ID)
expect(matchingEvent.properties.$survey_response).to.equal('Product analytics')
expect(matchingEvent.properties.$survey_response_1).to.equal('product_analytics')
expect(matchingEvent.properties.$survey_reasons.length).to.equal(1)
expect(matchingEvent.properties.$survey_reasons[0]).to.equal('Too expensive')
})

cy.get('.LemonModal').should('not.exist')
Expand All @@ -35,14 +38,14 @@ describe('Billing', () => {
it('Unsubscribe survey text area maintains unique state between product types', () => {
cy.get('[data-attr=more-button]').first().click()
cy.contains('.LemonButton', 'Unsubscribe').click()
cy.get('.LemonModal h3').should('contain', 'Why are you unsubscribing from Product analytics?')
cy.get('.LemonModal h3').should('contain', 'Unsubscribe from Product analytics')

cy.get('[data-attr=unsubscribe-reason-survey-textarea]').type('Product analytics')
cy.contains('.LemonModal .LemonButton', 'Cancel').click()

cy.get('[data-attr=more-button]').eq(1).click()
cy.contains('.LemonButton', 'Unsubscribe').click()
cy.get('.LemonModal h3').should('contain', 'Why are you unsubscribing from Session replay?')
cy.get('.LemonModal h3').should('contain', 'Unsubscribe from Session replay')
cy.get('[data-attr=unsubscribe-reason-survey-textarea]').type('Session replay')
cy.contains('.LemonModal .LemonButton', 'Cancel').click()

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 11 additions & 2 deletions frontend/src/lib/lemon-ui/LemonCheckbox/LemonCheckbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ export interface LemonCheckboxProps {
label?: string | JSX.Element
id?: string
className?: string
labelClassName?: string
fullWidth?: boolean
size?: 'small' | 'medium'
bordered?: boolean
/** @deprecated See https://github.com/PostHog/posthog/pull/9357#pullrequestreview-933783868. */
color?: string
dataAttr?: string
}

export interface BoxCSSProperties extends React.CSSProperties {
Expand All @@ -43,10 +45,12 @@ export function LemonCheckbox({
label,
id: rawId,
className,
labelClassName,
fullWidth,
bordered,
color,
size,
dataAttr,
}: LemonCheckboxProps): JSX.Element {
const indeterminate = checked === 'indeterminate'
disabled = disabled || !!disabledReason
Expand Down Expand Up @@ -80,6 +84,7 @@ export function LemonCheckbox({
size && `LemonCheckbox--${size}`,
className
)}
data-attr={dataAttr}
>
<input
className="LemonCheckbox__input"
Expand All @@ -94,8 +99,12 @@ export function LemonCheckbox({
id={id}
disabled={disabled}
/>
{/* eslint-disable-next-line react/forbid-dom-props */}
<label htmlFor={id} style={color ? ({ '--box-color': color } as BoxCSSProperties) : {}}>
<label
htmlFor={id}
/* eslint-disable-next-line react/forbid-dom-props */
style={color ? ({ '--box-color': color } as BoxCSSProperties) : {}}
className={labelClassName}
>
<svg
className="LemonCheckbox__box"
fill="none"
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/scenes/billing/AllProductsPlanComparison.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ export const AllProductsPlanComparison = ({
if (!plan.current_plan) {
setBillingProductLoading(product.type)
if (i < currentPlanIndex) {
setSurveyResponse(product.type, '$survey_response_1')
setSurveyResponse('$survey_response_1', product.type)
reportSurveyShown(UNSUBSCRIBE_SURVEY_ID, product.type)
reportBillingDowngradeClicked(product.type)
} else {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/scenes/billing/BillingProduct.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }):
<LemonButton
fullWidth
onClick={() => {
setSurveyResponse(product.type, '$survey_response_1')
setSurveyResponse('$survey_response_1', product.type)
reportSurveyShown(UNSUBSCRIBE_SURVEY_ID, product.type)
}}
>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/scenes/billing/BillingProductAddon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonTyp
<LemonButton
fullWidth
onClick={() => {
setSurveyResponse(addon.type, '$survey_response_1')
setSurveyResponse('$survey_response_1', addon.type)
reportSurveyShown(UNSUBSCRIBE_SURVEY_ID, addon.type)
}}
>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/scenes/billing/PlanComparison.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export const PlanComparison = ({
if (!plan.current_plan) {
setBillingProductLoading(product.type)
if (i < currentPlanIndex) {
setSurveyResponse(product.type, '$survey_response_1')
setSurveyResponse('$survey_response_1', product.type)
reportSurveyShown(UNSUBSCRIBE_SURVEY_ID, product.type)
reportBillingDowngradeClicked(product.type)
} else {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/scenes/billing/UnsubscribeCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const UnsubscribeCard = ({ product }: { product: BillingProductV2Type }):
type="secondary"
size="small"
onClick={() => {
setSurveyResponse(product.type, '$survey_response_1')
setSurveyResponse('$survey_response_1', product.type)
reportSurveyShown(UNSUBSCRIBE_SURVEY_ID, product.type)
}}
>
Expand Down
73 changes: 54 additions & 19 deletions frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import './UnsubscribeSurveyModal.scss'

import { LemonBanner, LemonButton, LemonModal, LemonTextArea, Link } from '@posthog/lemon-ui'
import { LemonBanner, LemonButton, LemonCheckbox, LemonLabel, LemonModal, LemonTextArea, Link } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'

import { BillingProductV2AddonType, BillingProductV2Type } from '~/types'
Expand All @@ -9,13 +9,26 @@ import { billingLogic } from './billingLogic'
import { billingProductLogic } from './billingProductLogic'
import { ExportsUnsubscribeTable, exportsUnsubscribeTableLogic } from './ExportsUnsubscribeTable'

const UNSUBSCRIBE_REASONS = [
'Too expensive',
'Not getting enough value',
'Not using the product',
'Found a better alternative',
'Poor customer support',
'Too difficult to use',
'Not enough hedgehogs',
'Other',
]

export const UnsubscribeSurveyModal = ({
product,
}: {
product: BillingProductV2Type | BillingProductV2AddonType
}): JSX.Element | null => {
const { surveyID, surveyResponse, isAddonProduct } = useValues(billingProductLogic({ product }))
const { setSurveyResponse, reportSurveyDismissed } = useActions(billingProductLogic({ product }))
const { setSurveyResponse, toggleSurveyReason, reportSurveyDismissed } = useActions(
billingProductLogic({ product })
)
const { deactivateProduct, resetUnsubscribeError } = useActions(billingLogic)
const { unsubscribeError, billingLoading, billing } = useValues(billingLogic)
const { unsubscribeDisabledReason, itemsToDisable } = useValues(exportsUnsubscribeTableLogic)
Expand All @@ -42,11 +55,7 @@ export const UnsubscribeSurveyModal = ({
resetUnsubscribeError()
}}
width="max(44vw)"
title={
billing?.subscription_level === 'paid'
? `Why are you ${actionVerb}?`
: `Why are you ${actionVerb} from ${product.name}?`
}
title={isAddonProduct ? action : `${action} from ${product.name}`}
footer={
<>
<LemonButton
Expand Down Expand Up @@ -75,33 +84,59 @@ export const UnsubscribeSurveyModal = ({
}
>
<div className="flex flex-col gap-3.5">
{unsubscribeError ? (
{unsubscribeError && (
<LemonBanner type="error">
<p>
{unsubscribeError.detail} {unsubscribeError.link}
</p>
</LemonBanner>
)}
{isAddonProduct ? (
<p>We're sorry to see you go! Please note, you'll lose access to the addon features immediately.</p>
) : (
<LemonBanner type="info">
<p>
Any outstanding invoices will be billed immediately.{' '}
<Link to={billing?.stripe_portal_url} target="_blank">
View invoices
</Link>
</p>
</LemonBanner>
<p>
We're sorry to see you go! Please note, you'll lose access to platform features and usage limits
will apply immediately. And if you have any outstanding invoices, they will be billed
immediately.{' '}
<Link to={billing?.stripe_portal_url} target="_blank">
View invoices
</Link>
</p>
)}

<LemonLabel>
{billing?.subscription_level === 'paid'
? `Why are you ${actionVerb}?`
: `Why are you ${actionVerb} from ${product.name}?`}{' '}
(select multiple)
</LemonLabel>
<div className="grid grid-cols-2 gap-2">
{UNSUBSCRIBE_REASONS.map((reason) => (
<LemonCheckbox
bordered
key={reason}
label={reason}
dataAttr={`unsubscribe-reason-${reason.toLowerCase().replace(' ', '-')}`}
checked={surveyResponse['$survey_reasons'].includes(reason)}
onChange={() => toggleSurveyReason(reason)}
className="w-full"
labelClassName="w-full"
/>
))}
</div>

<LemonTextArea
data-attr="unsubscribe-reason-survey-textarea"
placeholder={`Reason for ${actionVerb}...`}
placeholder="Share your feedback here so we can improve PostHog!"
value={surveyResponse['$survey_response']}
onChange={(value) => {
setSurveyResponse(value, '$survey_response')
setSurveyResponse('$survey_response', value)
}}
/>

<LemonBanner type="info">
<p>
{'Need to control your costs? Learn about ways to '}
{'Are you looking to control your costs? Learn about ways to '}
<Link
to="https://posthog.com/docs/billing/estimating-usage-costs#how-to-reduce-your-posthog-costs"
target="_blank"
Expand Down
21 changes: 15 additions & 6 deletions frontend/src/scenes/billing/billingProductLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ export const billingProductLogic = kea<billingProductLogicType>([
setShowTierBreakdown: (showTierBreakdown: boolean) => ({ showTierBreakdown }),
toggleIsPricingModalOpen: true,
toggleIsPlanComparisonModalOpen: (highlightedFeatureKey?: string) => ({ highlightedFeatureKey }),
setSurveyResponse: (surveyResponse: string, key: string) => ({ surveyResponse, key }),
setSurveyResponse: (key: string, value: string | string[]) => ({ key, value }),
toggleSurveyReason: (reason: string) => ({ reason }),
reportSurveyShown: (surveyID: string, productType: string) => ({ surveyID, productType }),
reportSurveySent: (surveyID: string, surveyResponse: Record<string, string>) => ({
reportSurveySent: (surveyID: string, surveyResponse: Record<string, string | string[]>) => ({
surveyID,
surveyResponse,
}),
Expand Down Expand Up @@ -110,10 +111,16 @@ export const billingProductLogic = kea<billingProductLogicType>([
},
],
surveyResponse: [
{},
{ $survey_reasons: [], $survey_response: '' } as { $survey_reasons: string[]; $survey_response: string },
{
setSurveyResponse: (state, { surveyResponse, key }) => {
return { ...state, [key]: surveyResponse }
setSurveyResponse: (state, { key, value }) => {
return { ...state, [key]: value }
},
toggleSurveyReason: (state, { reason }) => {
const reasons = state.$survey_reasons.includes(reason)
? state.$survey_reasons.filter((r) => r !== reason)
: [...state.$survey_reasons, reason]
return { ...state, $survey_reasons: reasons }
},
},
],
Expand Down Expand Up @@ -287,8 +294,10 @@ export const billingProductLogic = kea<billingProductLogicType>([
},
deactivateProductSuccess: async (_, breakpoint) => {
if (!values.unsubscribeError) {
const hasSurveyReasons = values.surveyResponse['$survey_reasons']?.length > 0
const textAreaNotEmpty = values.surveyResponse['$survey_response']?.length > 0
textAreaNotEmpty
const shouldReportSurvey = hasSurveyReasons || textAreaNotEmpty
shouldReportSurvey
? actions.reportSurveySent(values.surveyID, values.surveyResponse)
: actions.reportSurveyDismissed(values.surveyID)
}
Expand Down

0 comments on commit e98701b

Please sign in to comment.