-
Notifications
You must be signed in to change notification settings - Fork 2k
feat: plan switch v1 without confirmation #39131
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
Changes from 6 commits
c41e011
51af88a
903da96
49e2ffd
3979f00
30b4bd7
1bd6635
b91a944
7793eb9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like flaky test, unrelated change |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,9 +4,10 @@ import { useMemo } from 'react' | |
| import { IconCheckCircle, IconPlus } from '@posthog/icons' | ||
| import { LemonButton, LemonButtonProps, LemonTag, Tooltip } from '@posthog/lemon-ui' | ||
|
|
||
| import { TRIAL_CANCELLATION_SURVEY_ID, UNSUBSCRIBE_SURVEY_ID } from 'lib/constants' | ||
| import { FEATURE_FLAGS, TRIAL_CANCELLATION_SURVEY_ID, UNSUBSCRIBE_SURVEY_ID } from 'lib/constants' | ||
| import { dayjs } from 'lib/dayjs' | ||
| import { More } from 'lib/lemon-ui/LemonButton/More' | ||
| import { featureFlagLogic } from 'lib/logic/featureFlagLogic' | ||
| import { toSentenceCase } from 'lib/utils' | ||
|
|
||
| import { BillingProductV2AddonType } from '~/types' | ||
|
|
@@ -30,13 +31,23 @@ export const BillingProductAddonActions = ({ | |
| buttonSize, | ||
| ctaTextOverride, | ||
| }: BillingProductAddonActionsProps): JSX.Element => { | ||
| const { billing, billingError, timeTotalInSeconds, timeRemainingInSeconds } = useValues(billingLogic) | ||
| const { | ||
| billing, | ||
| billingError, | ||
| switchPlanLoading, | ||
| timeTotalInSeconds, | ||
| timeRemainingInSeconds, | ||
| currentPlatformAddon, | ||
| } = useValues(billingLogic) | ||
| const { switchFlatrateSubscriptionPlan } = useActions(billingLogic) | ||
| const { featureFlags } = useValues(featureFlagLogic) | ||
| const { | ||
| currentAndUpgradePlans, | ||
| billingProductLoading, | ||
| trialLoading, | ||
| isSubscribedToAnotherAddon, | ||
| isDataPipelinesDeprecated, | ||
| isLowerTierThanCurrentAddon, | ||
| } = useValues(billingProductLogic({ product: addon, productRef })) | ||
|
|
||
| const { toggleIsPricingModalOpen, reportSurveyShown, setSurveyResponse, initiateProductUpgrade, activateTrial } = | ||
|
|
@@ -53,6 +64,7 @@ export const BillingProductAddonActions = ({ | |
| [billing?.has_active_subscription, upgradePlan, timeRemainingInSeconds, timeTotalInSeconds] | ||
| ) | ||
| const isTrialEligible = !!addon.trial | ||
| const isSwitchPlanEnabled = !!featureFlags[FEATURE_FLAGS.SWITCH_SUBSCRIPTION_PLAN] | ||
|
|
||
| const renderSubscribedActions = (): JSX.Element | null => { | ||
| if (addon.contact_support) { | ||
|
|
@@ -196,6 +208,68 @@ export const BillingProductAddonActions = ({ | |
| return null | ||
| } | ||
|
|
||
| const renderDowngradeActions = (): JSX.Element | null => { | ||
| if (!upgradePlan || !currentPlatformAddon) { | ||
| return null | ||
| } | ||
|
|
||
| return ( | ||
| <More | ||
| overlay={ | ||
| <LemonButton | ||
| fullWidth | ||
| loading={switchPlanLoading === addon.type} | ||
| // TODO: Show confirmation modal with AddonFeatureLossNotice | ||
| onClick={() => | ||
| switchFlatrateSubscriptionPlan({ | ||
| from_product_key: String(currentPlatformAddon?.type), | ||
| from_plan_key: String(currentPlatformAddon?.plans[0].plan_key), | ||
| to_product_key: addon.type, | ||
| to_plan_key: String(upgradePlan?.plan_key), | ||
| }) | ||
|
Comment on lines
+225
to
+229
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Potential runtime error if currentPlatformAddon is undefined or plans array is empty. Add null safety checks. Prompt To Fix With AIThis is a comment left during a code review.
Path: frontend/src/scenes/billing/BillingProductAddonActions.tsx
Line: 225:229
Comment:
**logic:** Potential runtime error if currentPlatformAddon is undefined or plans array is empty. Add null safety checks.
How can I resolve this? If you propose a fix, please make it concise.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added check for |
||
| } | ||
| > | ||
| Downgrade | ||
| </LemonButton> | ||
| } | ||
| /> | ||
| ) | ||
| } | ||
|
|
||
| const renderUpgradeActions = (): JSX.Element | null => { | ||
| if (!upgradePlan || !currentPlatformAddon) { | ||
| return null | ||
| } | ||
|
|
||
| const showPricing = upgradePlan.flat_rate | ||
|
|
||
| return ( | ||
| <> | ||
| {showPricing && ( | ||
| <h4 className="leading-5 font-bold mb-0 flex gap-x-0.5"> | ||
| {formatFlatRate(Number(upgradePlan.unit_amount_usd), upgradePlan.unit)} | ||
| </h4> | ||
| )} | ||
|
|
||
| <LemonButton | ||
| type="primary" | ||
| loading={switchPlanLoading === addon.type} | ||
| onClick={() => | ||
| switchFlatrateSubscriptionPlan({ | ||
| from_product_key: String(currentPlatformAddon?.type), | ||
| from_plan_key: String(currentPlatformAddon?.plans[0].plan_key), | ||
| to_product_key: addon.type, | ||
| to_plan_key: String(upgradePlan?.plan_key), | ||
| }) | ||
|
Comment on lines
+259
to
+263
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Same issue as downgrade - needs null safety for currentPlatformAddon and plans[0] Prompt To Fix With AIThis is a comment left during a code review.
Path: frontend/src/scenes/billing/BillingProductAddonActions.tsx
Line: 259:263
Comment:
**logic:** Same issue as downgrade - needs null safety for currentPlatformAddon and plans[0]
How can I resolve this? If you propose a fix, please make it concise. |
||
| } | ||
| > | ||
| Upgrade | ||
| </LemonButton> | ||
| </> | ||
| // TODO: show prorated amount similar to renderPricingInfo | ||
| ) | ||
| } | ||
|
|
||
| let content | ||
| if (addon.subscribed && !addon.inclusion_only) { | ||
| content = renderSubscribedActions() | ||
|
|
@@ -219,6 +293,10 @@ export const BillingProductAddonActions = ({ | |
| // We don't allow multiple add-ons to be subscribed to at the same time so this checks if the customer is subscribed to another add-on | ||
| // TODO: add support for when a customer has a Paid Plan trial | ||
| content = renderPurchaseActions() | ||
| } else if (!billing?.trial && isSubscribedToAnotherAddon && isLowerTierThanCurrentAddon && isSwitchPlanEnabled) { | ||
| content = renderDowngradeActions() | ||
| } else if (!billing?.trial && isSubscribedToAnotherAddon && !isLowerTierThanCurrentAddon && isSwitchPlanEnabled) { | ||
| content = renderUpgradeActions() | ||
| } | ||
|
|
||
| return ( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ import { | |
| BillingPeriod, | ||
| BillingPlan, | ||
| BillingPlanType, | ||
| BillingProductV2AddonType, | ||
| BillingProductV2Type, | ||
| BillingType, | ||
| ProductKey, | ||
|
|
@@ -66,6 +67,13 @@ export interface BillingError { | |
| action: LemonButtonPropsBase | ||
| } | ||
|
|
||
| export type SwitchPlanPayload = { | ||
| from_product_key: string | ||
| from_plan_key: string | ||
| to_product_key: string | ||
| to_plan_key: string | ||
| } | ||
|
|
||
| const parseBillingResponse = (data: Partial<BillingType>): BillingType => { | ||
| if (data.billing_period) { | ||
| data.billing_period = { | ||
|
|
@@ -162,6 +170,7 @@ export const billingLogic = kea<billingLogicType>([ | |
| setComputedDiscount: (discount: number) => ({ discount }), | ||
| setCreditBrackets: (creditBrackets: any[]) => ({ creditBrackets }), | ||
| scrollToProduct: (productType: string) => ({ productType }), | ||
| setSwitchPlanLoading: (productKey: string | null) => ({ productKey }), | ||
| }), | ||
| connect(() => ({ | ||
| values: [ | ||
|
|
@@ -280,6 +289,12 @@ export const billingLogic = kea<billingLogicType>([ | |
| setCreditBrackets: (_, { creditBrackets }) => creditBrackets || [], | ||
| }, | ||
| ], | ||
| switchPlanLoading: [ | ||
| null as string | null, | ||
| { | ||
| setSwitchPlanLoading: (_, { productKey }) => productKey, | ||
| }, | ||
| ], | ||
| }), | ||
| lazyLoaders(({ actions, values }) => ({ | ||
| billing: [ | ||
|
|
@@ -374,6 +389,30 @@ export const billingLogic = kea<billingLogicType>([ | |
| return values.billing | ||
| } | ||
| }, | ||
| switchFlatrateSubscriptionPlan: async (data: SwitchPlanPayload, breakpoint) => { | ||
| try { | ||
| await api.create('api/billing/subscription/switch-plan', data) | ||
|
|
||
| const productDisplayName = data.to_product_key[0].toUpperCase() + data.to_product_key.slice(1) | ||
|
||
| lemonToast.success(`You're now on ${productDisplayName}`) | ||
|
|
||
| // Reload billing, user, and organization to get the updated available features | ||
| actions.loadBilling() | ||
| await breakpoint(2000) | ||
| actions.loadUser() | ||
| actions.loadCurrentOrganization() | ||
|
|
||
| return values.billing as BillingType | ||
| } catch (error: any) { | ||
| console.error(error) | ||
|
||
| lemonToast.error( | ||
| (error && error.detail) || | ||
| 'There was an error switching your plan. Please try again or contact support.' | ||
| ) | ||
| // Keep the current billing state on failure | ||
| return values.billing as BillingType | ||
| } | ||
| }, | ||
| }, | ||
| ], | ||
| billingError: [ | ||
|
|
@@ -500,6 +539,24 @@ export const billingLogic = kea<billingLogicType>([ | |
| ?.addons.find((addon) => addon.plans.find((plan) => plan.current_plan)) | ||
| }, | ||
| ], | ||
| platformAddons: [ | ||
| (s) => [s.billing], | ||
| (billing: BillingType): BillingProductV2AddonType[] => { | ||
| const platformProduct = billing?.products?.find( | ||
| (product: BillingProductV2Type) => product.type === ProductKey.PLATFORM_AND_SUPPORT | ||
| ) | ||
| return platformProduct?.addons ?? [] | ||
| }, | ||
| ], | ||
| currentPlatformAddon: [ | ||
| (s) => [s.billing], | ||
| (billing: BillingType): BillingProductV2AddonType | null => { | ||
| const platformProduct = billing?.products?.find( | ||
| (product: BillingProductV2Type) => product.type === ProductKey.PLATFORM_AND_SUPPORT | ||
| ) | ||
| return platformProduct?.addons?.find((addon: BillingProductV2AddonType) => !!addon.subscribed) || null | ||
| }, | ||
| ], | ||
| creditDiscount: [(s) => [s.computedDiscount], (computedDiscount) => computedDiscount || 0], | ||
| billingPlan: [ | ||
| (s) => [s.billing], | ||
|
|
@@ -693,6 +750,15 @@ export const billingLogic = kea<billingLogicType>([ | |
| posthog.capture('credits cta hero dismissed') | ||
| } | ||
| }, | ||
| switchFlatrateSubscriptionPlan: async (payload) => { | ||
| actions.setSwitchPlanLoading(payload.to_product_key) | ||
| }, | ||
| switchFlatrateSubscriptionPlanSuccess: () => { | ||
| actions.setSwitchPlanLoading(null) | ||
| }, | ||
| switchFlatrateSubscriptionPlanFailure: () => { | ||
| actions.setSwitchPlanLoading(null) | ||
| }, | ||
| loadBillingSuccess: async (_, breakpoint) => { | ||
| actions.registerInstrumentationProps() | ||
| actions.determineBillingAlert() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: Missing error handling for the switch_plan operation. Other billing endpoints in this file (like deactivate, get_invoices) have try-catch blocks to handle billing service errors gracefully and return appropriate error responses.
Prompt To Fix With AI
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the billing request fails, this will return a 500 error, and the user sees a toast "There was an error switching your plan. Please try again or contact support." This is handled in
billingLogic.tsxSo I don’t think we need to expose detailed errors from billing (like validation errors).