diff --git a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--dark.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--dark.png index 58021c0a239a1..92ddce1b5f945 100644 Binary files a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--dark.png and b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--light.png b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--light.png index b27c727a74240..53a95cf46f65c 100644 Binary files a/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--light.png and b/frontend/__snapshots__/scenes-other-billing-v2--billing-v-2--light.png differ diff --git a/frontend/src/scenes/billing/Billing.stories.tsx b/frontend/src/scenes/billing/Billing.stories.tsx index 2304bf5d691ed..139cc156c4bc1 100644 --- a/frontend/src/scenes/billing/Billing.stories.tsx +++ b/frontend/src/scenes/billing/Billing.stories.tsx @@ -17,7 +17,7 @@ const meta: Meta = { parameters: { layout: 'fullscreen', viewMode: 'story', - mockDate: '2023-05-25', + mockDate: '2024-03-10', }, decorators: [ mswDecorator({ diff --git a/frontend/src/scenes/billing/BillingProductAddon.tsx b/frontend/src/scenes/billing/BillingProductAddon.tsx index 2cfa9a0484a37..f4084a9336c1c 100644 --- a/frontend/src/scenes/billing/BillingProductAddon.tsx +++ b/frontend/src/scenes/billing/BillingProductAddon.tsx @@ -3,7 +3,7 @@ import { LemonButton, LemonSelectOptions, LemonTag, Link, Tooltip } from '@posth import { useActions, useValues } from 'kea' import { UNSUBSCRIBE_SURVEY_ID } from 'lib/constants' import { More } from 'lib/lemon-ui/LemonButton/More' -import { useRef } from 'react' +import { ReactNode, useMemo, useRef } from 'react' import { getProductIcon } from 'scenes/products/Products' import { BillingProductV2AddonType } from '~/types' @@ -13,9 +13,22 @@ import { billingProductLogic } from './billingProductLogic' import { ProductPricingModal } from './ProductPricingModal' import { UnsubscribeSurveyModal } from './UnsubscribeSurveyModal' +const formatFlatRate = (flatRate: number, unit: string | null): string | ReactNode => { + if (!unit) { + return `$${flatRate}` + } + return ( + + ${Number(flatRate)} + / + {unit} + + ) +} + export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonType }): JSX.Element => { const productRef = useRef(null) - const { billing, redirectPath, billingError } = useValues(billingLogic) + const { billing, redirectPath, billingError, daysTotal, daysRemaining } = useValues(billingLogic) const { isPricingModalOpen, currentAndUpgradePlans, surveyID, billingProductLoading } = useValues( billingProductLogic({ product: addon, productRef }) ) @@ -23,6 +36,24 @@ export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonTyp billingProductLogic({ product: addon }) ) + const upgradePlan = currentAndUpgradePlans?.upgradePlan + + const prorationAmount = useMemo( + () => + upgradePlan?.unit_amount_usd + ? (parseInt(upgradePlan?.unit_amount_usd) * ((daysRemaining || 1) / (daysTotal || 1))).toFixed(0) + : 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] + ) + const productType = { plural: `${addon.unit}s`, singular: addon.unit } const tierDisplayOptions: LemonSelectOptions = [ { label: `Per ${productType.singular}`, value: 'individual' }, @@ -81,66 +112,85 @@ export const BillingProductAddon = ({ addon }: { addon: BillingProductV2AddonTyp )} -
- {addon.docs_url && ( - } size="small" to={addon.docs_url} tooltip="Read the docs" /> - )} - {addon.subscribed && !addon.inclusion_only ? ( - <> - - { - setSurveyResponse(addon.type, '$survey_response_1') - reportSurveyShown(UNSUBSCRIBE_SURVEY_ID, addon.type) - }} - > - Remove addon - - - } +
+
+ {addon.docs_url && ( + } + size="small" + to={addon.docs_url} + tooltip="Read the docs" /> - - ) : addon.included_with_main_product ? ( - }> - Included with plan - - ) : ( - <> - {currentAndUpgradePlans?.upgradePlan?.flat_rate ? ( -

- ${Number(currentAndUpgradePlans?.upgradePlan?.unit_amount_usd)} - / - {currentAndUpgradePlans?.upgradePlan?.unit} -

- ) : ( - { - toggleIsPricingModalOpen() - }} - > - View pricing - - )} - {!addon.inclusion_only && ( - } - size="small" - disableClientSideRouting - disabledReason={billingError && billingError.message} - loading={billingProductLoading === addon.type} - onClick={() => - initiateProductUpgrade(addon, currentAndUpgradePlans?.upgradePlan, redirectPath) + )} + {addon.subscribed && !addon.inclusion_only ? ( + <> + + { + setSurveyResponse(addon.type, '$survey_response_1') + reportSurveyShown(UNSUBSCRIBE_SURVEY_ID, addon.type) + }} + > + Remove addon + + } - > - Add - - )} - + /> + + ) : addon.included_with_main_product ? ( + }> + Included with plan + + ) : ( + <> + {currentAndUpgradePlans?.upgradePlan?.flat_rate ? ( +

+ + {formatFlatRate(Number(upgradePlan?.unit_amount_usd), upgradePlan?.unit)} + +

+ ) : ( + { + toggleIsPricingModalOpen() + }} + > + View pricing + + )} + {!addon.inclusion_only && ( + } + size="small" + disableClientSideRouting + disabledReason={billingError && billingError.message} + loading={billingProductLoading === addon.type} + onClick={() => + initiateProductUpgrade( + addon, + currentAndUpgradePlans?.upgradePlan, + redirectPath + ) + } + > + Add + + )} + + )} +
+ {!addon.inclusion_only && isProrated && ( +

+ ${prorationAmount} charged today (pro-rated), +
+ then {formatFlatRate(Number(upgradePlan?.unit_amount_usd), upgradePlan?.unit)} starting next + invoice. +

)}
diff --git a/frontend/src/scenes/billing/billingProductLogic.ts b/frontend/src/scenes/billing/billingProductLogic.ts index e5bb93480793b..744e30d1bc2dd 100644 --- a/frontend/src/scenes/billing/billingProductLogic.ts +++ b/frontend/src/scenes/billing/billingProductLogic.ts @@ -1,7 +1,6 @@ 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' @@ -37,8 +36,6 @@ export const billingProductLogic = kea([ 'setScrollToProductKey', 'deactivateProductSuccess', ], - confirmUpgradeModalLogic, - ['showConfirmUpgradeModal'], ], }), actions({ @@ -331,15 +328,7 @@ export const billingProductLogic = kea([ }, 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) - } + actions.handleProductUpgrade(product, plan, redirectPath) }, handleProductUpgrade: ({ plan, product, redirectPath }) => { window.location.href = `/api/billing/activation?products=${product.type}:${plan?.plan_key}${