Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add reasons to unsub modal #24109

Merged
merged 13 commits into from
Aug 2, 2024
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',
Copy link
Member

Choose a reason for hiding this comment

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

LOL

'Other',
zlwaterfield marked this conversation as resolved.
Show resolved Hide resolved
]

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>
Copy link
Member

Choose a reason for hiding this comment

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

There's some extra-large margin here due to p's implicit margin-bottom:

Suggested change
<p>
<p className="mb-0">

(this is important so that the modal doesn't require scrolling)

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)
zlwaterfield marked this conversation as resolved.
Show resolved Hide resolved
</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
Loading