Skip to content

Commit

Permalink
feat: add confirm upgrade modal for flat rate subscriptions (#22449)
Browse files Browse the repository at this point in the history
* Add confirm upgrade modal

* move to a new confirm upgrade logic

* make the sub unit dynamic

* remove old code

* add prorated copy

* Move more code into logic

* Improve how teams plans features are displayed
  • Loading branch information
zlwaterfield authored May 24, 2024
1 parent ab2fd5f commit 82be493
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 35 deletions.
2 changes: 2 additions & 0 deletions frontend/src/layout/GlobalModals.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { LemonModal } from '@posthog/lemon-ui'
import { actions, kea, path, reducers, useActions, useValues } from 'kea'
import { ConfirmUpgradeModal } from 'lib/components/ConfirmUpgradeModal/ConfirmUpgradeModal'
import { HedgehogBuddyWithLogic } from 'lib/components/HedgehogBuddy/HedgehogBuddyWithLogic'
import { TimeSensitiveAuthenticationModal } from 'lib/components/TimeSensitiveAuthentication/TimeSensitiveAuthentication'
import { UpgradeModal } from 'lib/components/UpgradeModal/UpgradeModal'
Expand Down Expand Up @@ -52,6 +53,7 @@ export function GlobalModals(): JSX.Element {
<CreateOrganizationModal isVisible={isCreateOrganizationModalShown} onClose={hideCreateOrganizationModal} />
<CreateProjectModal isVisible={isCreateProjectModalShown} onClose={hideCreateProjectModal} />
<UpgradeModal />
<ConfirmUpgradeModal />
<TimeSensitiveAuthenticationModal />

{user && user.organization?.enforce_2fa && !user.is_2fa_enabled && (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { IconCheckCircle } from '@posthog/icons'
import { LemonButton, LemonModal, Tooltip } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { useMemo } from 'react'
import { billingLogic } from 'scenes/billing/billingLogic'

import { confirmUpgradeModalLogic } from './confirmUpgradeModalLogic'

export function ConfirmUpgradeModal(): JSX.Element {
const { upgradePlan } = useValues(confirmUpgradeModalLogic)
const { daysRemaining, daysTotal, billing } = useValues(billingLogic)
const { hideConfirmUpgradeModal, confirm, cancel } = useActions(confirmUpgradeModalLogic)

const prorationAmount = useMemo(
() =>
upgradePlan?.unit_amount_usd
? (parseInt(upgradePlan?.unit_amount_usd) * ((daysRemaining || 1) / (daysTotal || 1))).toFixed(2)
: 0,
[upgradePlan, daysRemaining, daysTotal]
)

const isProrated = useMemo(
() =>
billing?.has_active_subscription && upgradePlan?.unit_amount_usd
? prorationAmount !== parseInt(upgradePlan?.unit_amount_usd || '')
: false,
[billing?.has_active_subscription, prorationAmount]
)

return (
<LemonModal
onClose={hideConfirmUpgradeModal}
isOpen={!!upgradePlan}
closable={false}
title={`Ready to subscribe to the ${upgradePlan?.name}?`}
footer={
<>
<LemonButton type="secondary" onClick={() => cancel()}>
Cancel
</LemonButton>
<LemonButton type="primary" onClick={() => confirm()}>
Sign me up
</LemonButton>
</>
}
>
<div className="max-w-140">
<p>
Woo! You're gonna love the {upgradePlan?.name}. We're just confirming that this is a $
{Number(upgradePlan?.unit_amount_usd)} / {upgradePlan?.unit} subscription.{' '}
{isProrated
? `The first payment will be prorated to $${prorationAmount} and it will be charged immediately.`
: 'The first payment will be charged immediately.'}
</p>
{upgradePlan && upgradePlan?.features?.length > 1 && (
<div>
<p className="ml-0 mb-2 max-w-200">Here are the features included:</p>
<div className="grid grid-cols-2 gap-x-4">
{upgradePlan?.features.map((feature, index) => (
<div className="flex gap-x-2 items-center mb-2" key={'addon-features-' + index}>
<IconCheckCircle className="text-success" />
<Tooltip key={feature.key} title={feature.description}>
<b>
{feature.name}
{feature.note ? ': ' + feature.note : ''}
</b>
</Tooltip>
</div>
))}
</div>
</div>
)}
</div>
</LemonModal>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { actions, kea, listeners, path, reducers } from 'kea'

import { BillingV2PlanType } from '~/types'

import type { confirmUpgradeModalLogicType } from './confirmUpgradeModalLogicType'

export const confirmUpgradeModalLogic = kea<confirmUpgradeModalLogicType>([
path(['lib', 'components', 'ConfirmUpgradeModal', 'confirmUpgradeModalLogic']),
actions({
showConfirmUpgradeModal: (
upgradePlan: BillingV2PlanType,
confirmCallback: () => void,
cancelCallback: () => void
) => ({
upgradePlan,
confirmCallback,
cancelCallback,
}),
hideConfirmUpgradeModal: true,
confirm: true,
cancel: true,
}),
reducers({
upgradePlan: [
null as BillingV2PlanType | null,
{
showConfirmUpgradeModal: (_, { upgradePlan }) => upgradePlan,
hideConfirmUpgradeModal: () => null,
},
],
confirmCallback: [
null as (() => void) | null,
{
showConfirmUpgradeModal: (_, { confirmCallback }) => confirmCallback,
hideConfirmUpgradeModal: () => null,
},
],
cancelCallback: [
null as (() => void) | null,
{
showConfirmUpgradeModal: (_, { cancelCallback }) => cancelCallback,
hideConfirmUpgradeModal: () => null,
},
],
}),
listeners(({ actions, values }) => ({
confirm: async (_, breakpoint) => {
await breakpoint(100)
if (values.confirmCallback) {
values.confirmCallback()
}
actions.hideConfirmUpgradeModal()
},
cancel: async (_, breakpoint) => {
await breakpoint(100)
if (values.cancelCallback) {
values.cancelCallback()
}
actions.hideConfirmUpgradeModal()
},
})),
])
47 changes: 24 additions & 23 deletions frontend/src/scenes/billing/BillingProductAddon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonTyp
const { isPricingModalOpen, currentAndUpgradePlans, surveyID, billingProductLoading } = useValues(
billingProductLogic({ product: addon, productRef })
)
const { toggleIsPricingModalOpen, reportSurveyShown, setSurveyResponse, setBillingProductLoading } = useActions(
const { toggleIsPricingModalOpen, reportSurveyShown, setSurveyResponse, initiateProductUpgrade } = useActions(
billingProductLogic({ product: addon })
)

Expand All @@ -33,7 +33,10 @@ export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonTyp
}

// Filter out the addon itself from the features list
const addonFeatures = addon.features?.filter((feature) => feature.name !== addon.name)
const addonFeatures =
currentAndUpgradePlans?.upgradePlan?.features ||
currentAndUpgradePlans?.currentPlan?.features ||
addon.features?.filter((feature) => feature.name !== addon.name)

const is_enhanced_persons_og_customer =
addon.type === 'enhanced_persons' &&
Expand Down Expand Up @@ -127,15 +130,12 @@ export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonTyp
type="primary"
icon={<IconPlus />}
size="small"
to={`/api/billing-v2/activation?products=${addon.type}:${
currentAndUpgradePlans?.upgradePlan?.plan_key
}${redirectPath && `&redirect_path=${redirectPath}`}`}
disableClientSideRouting
disabledReason={billingError && billingError.message}
loading={billingProductLoading === addon.type}
onClick={() => {
setBillingProductLoading(addon.type)
}}
onClick={() =>
initiateProductUpgrade(addon, currentAndUpgradePlans?.upgradePlan, redirectPath)
}
>
Add
</LemonButton>
Expand All @@ -148,21 +148,22 @@ export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonTyp
{addonFeatures?.length > 1 && (
<div>
<p className="ml-0 mb-2 max-w-200">Features included:</p>
{addonFeatures?.map((feature, i) => {
return (
i < 6 && (
<div
className="flex gap-x-2 items-center mb-2"
key={'addon-features-' + addon.type + i}
>
<IconCheckCircle className="text-success" />
<Tooltip key={feature.key} title={feature.description}>
<b>{feature.name} </b>
</Tooltip>
</div>
)
)
})}
<div className="grid grid-cols-2 gap-x-4">
{addonFeatures.map((feature, index) => (
<div
className="flex gap-x-2 items-center mb-2"
key={'addon-features-' + addon.type + index}
>
<IconCheckCircle className="text-success" />
<Tooltip key={feature.key} title={feature.description}>
<b>
{feature.name}
{feature.note ? ': ' + feature.note : ''}
</b>
</Tooltip>
</div>
))}
</div>
</div>
)}
</div>
Expand Down
13 changes: 2 additions & 11 deletions frontend/src/scenes/billing/PlanComparison.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import clsx from 'clsx'
import { useActions, useValues } from 'kea'
import { BillingUpgradeCTA } from 'lib/components/BillingUpgradeCTA'
import { UNSUBSCRIBE_SURVEY_ID } from 'lib/constants'
import { dayjs } from 'lib/dayjs'
import { Tooltip } from 'lib/lemon-ui/Tooltip'
import { eventUsageLogic } from 'lib/utils/eventUsageLogic'
import React, { useState } from 'react'
Expand Down Expand Up @@ -120,17 +119,12 @@ export const PlanComparison = ({
return null
}
const fullyFeaturedPlan = plans[plans.length - 1]
const { billing, redirectPath } = useValues(billingLogic)
const { billing, redirectPath, daysRemaining, daysTotal } = useValues(billingLogic)
const { width, ref: planComparisonRef } = useResizeObserver()
const { reportBillingUpgradeClicked } = useActions(eventUsageLogic)
const currentPlanIndex = plans.findIndex((plan) => plan.current_plan)
const { surveyID, comparisonModalHighlightedFeatureKey } = useValues(billingProductLogic({ product }))
const { reportSurveyShown, setSurveyResponse } = useActions(billingProductLogic({ product }))
const billingDaysRemaining = billing?.billing_period?.current_period_end.diff(dayjs(), 'days')
const billingDaysTotal = billing?.billing_period?.current_period_end.diff(
billing.billing_period?.current_period_start,
'days'
)

const upgradeButtons = plans?.map((plan, i) => {
return (
Expand Down Expand Up @@ -223,10 +217,7 @@ export const PlanComparison = ({
<td className="font-bold">Monthly {product.tiered && 'base '} price</td>
{plans?.map((plan) => {
const prorationAmount = plan.unit_amount_usd
? (
parseInt(plan.unit_amount_usd) *
((billingDaysRemaining || 1) / (billingDaysTotal || 1))
).toFixed(2)
? (parseInt(plan.unit_amount_usd) * ((daysRemaining || 1) / (daysTotal || 1))).toFixed(2)
: 0
const isProrated =
billing?.has_active_subscription && plan.unit_amount_usd
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/scenes/billing/billingLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,31 @@ export const billingLogic = kea<billingLogicType>([
setUnsubscribeError: (_, { error }) => error,
},
],
daysRemaining: [
0,
{
loadBillingSuccess: (_, { billing }) => {
if (!billing?.billing_period) {
return 0
}
return billing.billing_period.current_period_end.diff(dayjs(), 'days')
},
},
],
daysTotal: [
0,
{
loadBillingSuccess: (_, { billing }) => {
if (!billing?.billing_period) {
return 0
}
return billing.billing_period.current_period_end.diff(
billing.billing_period.current_period_start,
'days'
)
},
},
],
}),
loaders(({ actions, values }) => ({
billing: [
Expand Down
40 changes: 39 additions & 1 deletion frontend/src/scenes/billing/billingProductLogic.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { LemonDialog } from '@posthog/lemon-ui'
import { actions, connect, events, kea, key, listeners, path, props, reducers, selectors } from 'kea'
import { forms } from 'kea-forms'
import { confirmUpgradeModalLogic } from 'lib/components/ConfirmUpgradeModal/confirmUpgradeModalLogic'
import posthog from 'posthog-js'
import React from 'react'

Expand Down Expand Up @@ -36,6 +37,8 @@ export const billingProductLogic = kea<billingProductLogicType>([
'setScrollToProductKey',
'deactivateProductSuccess',
],
confirmUpgradeModalLogic,
['showConfirmUpgradeModal'],
],
}),
actions({
Expand All @@ -53,7 +56,25 @@ export const billingProductLogic = kea<billingProductLogicType>([
}),
reportSurveyDismissed: (surveyID: string) => ({ surveyID }),
setSurveyID: (surveyID: string) => ({ surveyID }),
setBillingProductLoading: (productKey: string) => ({ productKey }),
setBillingProductLoading: (productKey: string | null) => ({ productKey }),
initiateProductUpgrade: (
product: BillingProductV2Type | BillingProductV2AddonType,
plan: BillingV2PlanType,
redirectPath?: string
) => ({
plan,
product,
redirectPath,
}),
handleProductUpgrade: (
product: BillingProductV2Type | BillingProductV2AddonType,
plan: BillingV2PlanType,
redirectPath?: string
) => ({
plan,
product,
redirectPath,
}),
}),
reducers({
billingLimitInput: [
Expand Down Expand Up @@ -308,6 +329,23 @@ export const billingProductLogic = kea<billingProductLogicType>([
}
}
},
initiateProductUpgrade: ({ plan, product, redirectPath }) => {
actions.setBillingProductLoading(product.type)
if (values.currentAndUpgradePlans.upgradePlan?.flat_rate) {
actions.showConfirmUpgradeModal(
values.currentAndUpgradePlans.upgradePlan,
() => actions.handleProductUpgrade(product, plan, redirectPath),
() => actions.setBillingProductLoading(null)
)
} else {
actions.handleProductUpgrade(product, plan, redirectPath)
}
},
handleProductUpgrade: ({ plan, product, redirectPath }) => {
window.location.href = `/api/billing-v2/activation?products=${product.type}:${plan?.plan_key}${
redirectPath && `&redirect_path=${redirectPath}`
}`
},
})),
forms(({ actions, props, values }) => ({
billingLimitInput: {
Expand Down

0 comments on commit 82be493

Please sign in to comment.