From 526d6f0e7c47f3102b50d46e5dc4e8130ac5fe8d Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 18 Mar 2026 16:48:49 -0300 Subject: [PATCH 1/6] refactor: extract Payment types, constants, and presentational components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split the monolithic Payment.js into smaller focused modules: - types.ts: Chargebee SDK global types and shared PricingFeature type - constants.ts: URLs, support email, icon colours - pricingFeatures.tsx: startup and enterprise feature lists - PricingFeaturesList.tsx: feature list rendering - PricingToggle.tsx: annual/monthly toggle - PricingPanel.tsx: pricing card with plan details and CTA All components are pure presentational — they receive data via props and contain no business logic or store dependencies. Ref: #6319 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../modals/payment/PricingFeaturesList.tsx | 28 +++ .../modals/payment/PricingPanel.tsx | 162 ++++++++++++++++++ .../modals/payment/PricingToggle.tsx | 34 ++++ .../components/modals/payment/constants.ts | 11 ++ .../components/modals/payment/hooks/index.ts | 2 + .../payment/hooks/useChargebeeCheckout.ts | 44 +++++ .../modals/payment/hooks/usePaymentState.ts | 31 ++++ .../modals/payment/pricingFeatures.tsx | 75 ++++++++ .../web/components/modals/payment/types.ts | 37 ++++ 9 files changed, 424 insertions(+) create mode 100644 frontend/web/components/modals/payment/PricingFeaturesList.tsx create mode 100644 frontend/web/components/modals/payment/PricingPanel.tsx create mode 100644 frontend/web/components/modals/payment/PricingToggle.tsx create mode 100644 frontend/web/components/modals/payment/constants.ts create mode 100644 frontend/web/components/modals/payment/hooks/index.ts create mode 100644 frontend/web/components/modals/payment/hooks/useChargebeeCheckout.ts create mode 100644 frontend/web/components/modals/payment/hooks/usePaymentState.ts create mode 100644 frontend/web/components/modals/payment/pricingFeatures.tsx create mode 100644 frontend/web/components/modals/payment/types.ts diff --git a/frontend/web/components/modals/payment/PricingFeaturesList.tsx b/frontend/web/components/modals/payment/PricingFeaturesList.tsx new file mode 100644 index 000000000000..0360f134481e --- /dev/null +++ b/frontend/web/components/modals/payment/PricingFeaturesList.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import Icon from 'components/Icon' +import { PRIMARY_ICON_COLOR } from './constants' +import { PricingFeature } from './types' + +export type PricingFeaturesListProps = { + features: PricingFeature[] +} + +export const PricingFeaturesList = ({ features }: PricingFeaturesListProps) => { + return ( + + ) +} diff --git a/frontend/web/components/modals/payment/PricingPanel.tsx b/frontend/web/components/modals/payment/PricingPanel.tsx new file mode 100644 index 000000000000..c08388e3617e --- /dev/null +++ b/frontend/web/components/modals/payment/PricingPanel.tsx @@ -0,0 +1,162 @@ +import React, { ReactNode } from 'react' +import classNames from 'classnames' +import Icon, { IconName } from 'components/Icon' +import Button from 'components/base/forms/Button' +import { PricingFeaturesList } from './PricingFeaturesList' +import { PaymentButton } from './PaymentButton' +import { openChat } from 'common/loadChat' +import { PricingFeature } from './types' + +export type PricingPanelProps = { + title: string + icon?: string + iconFill?: string + priceMonthly?: string + priceYearly?: string + isYearly: boolean + viewOnly?: boolean + chargebeePlanId?: string + isPurchased?: boolean + isEnterprise?: boolean + isDisableAccount?: string + features: PricingFeature[] + headerContent?: ReactNode + onContactSales?: () => void +} + +export const PricingPanel = ({ + chargebeePlanId, + features, + headerContent, + icon = 'flash', + iconFill, + isDisableAccount, + isEnterprise, + isPurchased, + isYearly, + onContactSales, + priceMonthly, + priceYearly, + title, + viewOnly, +}: PricingPanelProps) => { + return ( + +
+
+
+
+ {headerContent && ( + + {headerContent} + + )} + + +

+ {title} +

+
+ + {priceYearly && priceMonthly && ( + +
$
+

+ {isYearly ? priceYearly : priceMonthly}{' '} +

/mo
+ +
+ )} + + {isEnterprise && ( + +
+ Maximum security and control +
+
+ )} +
+ +
+ +
+ {!viewOnly && !isEnterprise && chargebeePlanId && ( + <> + + {isPurchased ? 'Purchased' : '14 Day Free Trial'} + + + )} + + {!viewOnly && isEnterprise && ( + + )} +
+
+
+ +
+
+ All from{' '} + + {isEnterprise ? 'Start-Up,' : 'Free,'} + {' '} + plus +
+ +
+
+ + ) +} diff --git a/frontend/web/components/modals/payment/PricingToggle.tsx b/frontend/web/components/modals/payment/PricingToggle.tsx new file mode 100644 index 000000000000..4b76db60772c --- /dev/null +++ b/frontend/web/components/modals/payment/PricingToggle.tsx @@ -0,0 +1,34 @@ +import classNames from 'classnames' +import Switch from 'components/Switch' + +export type PricingToggleProps = { + isYearly: boolean + onChange: (isYearly: boolean) => void +} + +export const PricingToggle = ({ isYearly, onChange }: PricingToggleProps) => { + return ( +
+
+ Pay Yearly (Save 10%) +
+ { + onChange(!isYearly) + }} + /> +
+ Pay Monthly +
+
+ ) +} diff --git a/frontend/web/components/modals/payment/constants.ts b/frontend/web/components/modals/payment/constants.ts new file mode 100644 index 000000000000..ffcd3cb25750 --- /dev/null +++ b/frontend/web/components/modals/payment/constants.ts @@ -0,0 +1,11 @@ +export const ENTERPRISE_ICON_COLOR = '#F7D56E' +export const PRIMARY_ICON_COLOR = '#27AB95' + +// URLs +export const CONTACT_US_URL = 'https://www.flagsmith.com/contact-us' +export const ON_PREMISE_HOSTING_URL = + 'https://www.flagsmith.com/on-premises-and-private-cloud-hosting' + +// Support +export const SUPPORT_EMAIL = 'support@flagsmith.com' +export const SUPPORT_EMAIL_URL = 'mailto:support@flagsmith.com' diff --git a/frontend/web/components/modals/payment/hooks/index.ts b/frontend/web/components/modals/payment/hooks/index.ts new file mode 100644 index 000000000000..8ffd804fc664 --- /dev/null +++ b/frontend/web/components/modals/payment/hooks/index.ts @@ -0,0 +1,2 @@ +export { usePaymentState } from './usePaymentState' +export { useChargebeeCheckout } from './useChargebeeCheckout' diff --git a/frontend/web/components/modals/payment/hooks/useChargebeeCheckout.ts b/frontend/web/components/modals/payment/hooks/useChargebeeCheckout.ts new file mode 100644 index 000000000000..592ea5883e06 --- /dev/null +++ b/frontend/web/components/modals/payment/hooks/useChargebeeCheckout.ts @@ -0,0 +1,44 @@ +import { useState } from 'react' +// @ts-ignore +import _data from 'common/data/base/_data' + +type UseChargebeeCheckoutParams = { + organisationId: number | undefined + onSuccess?: () => void +} + +type ChargebeeCheckout = { + openCheckout: (planId: string) => void + isLoading: boolean +} + +export const useChargebeeCheckout = ({ + onSuccess, + organisationId, +}: UseChargebeeCheckoutParams): ChargebeeCheckout => { + const [isLoading, setIsLoading] = useState(false) + + const openCheckout = (planId: string) => { + if (!organisationId) return + + setIsLoading(true) + Chargebee.getInstance().openCheckout({ + hostedPage() { + return _data.post( + `${Project.api}organisations/${organisationId}/get-hosted-page-url-for-subscription-upgrade/`, + { plan_id: planId }, + ) + }, + success: (res: any) => { + AppActions.updateSubscription(res) + onSuccess?.() + setIsLoading(false) + }, + }) + } + + return { + isLoading, + openCheckout, + } +} diff --git a/frontend/web/components/modals/payment/hooks/usePaymentState.ts b/frontend/web/components/modals/payment/hooks/usePaymentState.ts new file mode 100644 index 000000000000..f1f5d1c73e01 --- /dev/null +++ b/frontend/web/components/modals/payment/hooks/usePaymentState.ts @@ -0,0 +1,31 @@ +import { useState } from 'react' +import AccountStore from 'common/stores/account-store' + +type PaymentState = { + organisation: any + plan: string + isAWS: boolean + hasActiveSubscription: boolean + yearly: boolean + setYearly: (yearly: boolean) => void +} + +export const usePaymentState = (): PaymentState => { + const [yearly, setYearly] = useState(true) + + const organisation = AccountStore.getOrganisation() + const plan = organisation?.subscription?.plan ?? '' + const isAWS = organisation?.subscription?.payment_method === 'AWS_MARKETPLACE' + const hasActiveSubscription = !!AccountStore.getOrganisationPlan( + organisation?.id, + ) + + return { + hasActiveSubscription, + isAWS, + organisation, + plan, + setYearly, + yearly, + } +} diff --git a/frontend/web/components/modals/payment/pricingFeatures.tsx b/frontend/web/components/modals/payment/pricingFeatures.tsx new file mode 100644 index 000000000000..90b936dfeca7 --- /dev/null +++ b/frontend/web/components/modals/payment/pricingFeatures.tsx @@ -0,0 +1,75 @@ +import { PricingFeature } from './types' +import { ENTERPRISE_ICON_COLOR } from './constants' + +export const startupFeatures: PricingFeature[] = [ + { + text: ( + <> + Up to + 1,000,000 Requests per month + + ), + }, + { + text: ( + <> + 3 Team members + + ), + }, + { + text: 'Unlimited projects', + }, + { + text: 'Email technical support', + }, + { + text: 'Scheduled flags', + }, + { + text: 'Two-factor authentication (2FA)', + }, +] + +export const enterpriseFeatures: PricingFeature[] = [ + { + iconFill: ENTERPRISE_ICON_COLOR, + text: ( + <> + 5,000,000+ requests per month + + ), + }, + { + iconFill: ENTERPRISE_ICON_COLOR, + text: ( + <> + 20+ Team members + + ), + }, + { + iconFill: ENTERPRISE_ICON_COLOR, + text: 'Advanced hosting options', + }, + { + iconFill: ENTERPRISE_ICON_COLOR, + text: 'Priority real time technical support with the engineering team over Slack or Discord', + }, + { + iconFill: ENTERPRISE_ICON_COLOR, + text: 'Governance features – roles, permissions, change requests, audit logs', + }, + { + iconFill: ENTERPRISE_ICON_COLOR, + text: 'Features for maximum security', + }, + { + iconFill: ENTERPRISE_ICON_COLOR, + text: 'Optional on premises installation', + }, + { + iconFill: ENTERPRISE_ICON_COLOR, + text: 'Onboarding & training', + }, +] diff --git a/frontend/web/components/modals/payment/types.ts b/frontend/web/components/modals/payment/types.ts new file mode 100644 index 000000000000..ae17abf4efc4 --- /dev/null +++ b/frontend/web/components/modals/payment/types.ts @@ -0,0 +1,37 @@ +import { ReactNode } from 'react' + +// ============================================================================ +// Global Declarations +// ============================================================================ + +// Chargebee SDK - Global type declaration for the Chargebee payment integration +type ChargebeeInstance = { + openCheckout: (config: { + hostedPage: () => Promise + success: (res: any) => void + }) => void + setCheckoutCallbacks?: (fn: () => { success: (id: string) => void }) => void + getCart: () => { + setCustomer: (customer: { cf_tid?: string }) => void + } +} + +type ChargebeeSDK = { + init: (config: { site: string }) => void + registerAgain: () => void + getInstance: () => ChargebeeInstance +} + +declare global { + const Chargebee: ChargebeeSDK +} + +// ============================================================================ +// Shared Types (used by multiple components) +// ============================================================================ + +export type PricingFeature = { + icon?: string + iconFill?: string + text: ReactNode +} From ad410071caad918eff5c64210aaa5f557a3864cd Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 18 Mar 2026 16:49:18 -0300 Subject: [PATCH 2/6] refactor: add Payment and PaymentButton components using extracted hooks Wire up the main Payment component and PaymentButton to use the extracted hooks (usePaymentState, useChargebeeCheckout) instead of directly accessing Flux stores. Add index.tsx barrel export with Chargebee async script loader wrapper. Ref: #6319 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web/components/modals/payment/Payment.tsx | 128 ++++++++++++++++++ .../modals/payment/PaymentButton.tsx | 50 +++++++ .../web/components/modals/payment/index.tsx | 63 +++++++++ 3 files changed, 241 insertions(+) create mode 100644 frontend/web/components/modals/payment/Payment.tsx create mode 100644 frontend/web/components/modals/payment/PaymentButton.tsx create mode 100644 frontend/web/components/modals/payment/index.tsx diff --git a/frontend/web/components/modals/payment/Payment.tsx b/frontend/web/components/modals/payment/Payment.tsx new file mode 100644 index 000000000000..aafdc8577129 --- /dev/null +++ b/frontend/web/components/modals/payment/Payment.tsx @@ -0,0 +1,128 @@ +import React, { FC, useEffect } from 'react' +import Constants from 'common/constants' +import InfoMessage from 'components/InfoMessage' +import BlockedOrgInfo from 'components/BlockedOrgInfo' +import { PricingToggle } from './PricingToggle' +import { PricingPanel } from './PricingPanel' +import { startupFeatures, enterpriseFeatures } from './pricingFeatures' +import { + CONTACT_US_URL, + ON_PREMISE_HOSTING_URL, + SUPPORT_EMAIL, + SUPPORT_EMAIL_URL, +} from './constants' +import { usePaymentState } from './hooks' + +type PaymentProps = { + viewOnly?: boolean + isDisableAccountText?: string +} + +export const Payment: FC = ({ + isDisableAccountText, + viewOnly, +}) => { + const { isAWS, plan, setYearly, yearly } = usePaymentState() + + useEffect(() => { + API.trackPage(Constants.modals.PAYMENT) + }, []) + + if (isAWS) { + return ( +
+ + Customers with AWS Marketplace subscriptions will need to{' '} + + contact us + + +
+ ) + } + + return ( +
+
+ + {isDisableAccountText && ( +
+
+

+ {isDisableAccountText}{' '} + + {SUPPORT_EMAIL} + +

+
+
+ +
+
+ )} +
+ + + + + + + + Optional{' '} + + On Premise + {' '} + or{' '} + + Private Cloud + {' '} + Install + + } + /> + +
+ *Need something in-between our Enterprise plan for users or API + limits? +
+ Reach out to us and we'll help you out +
+
+
+
+ ) +} diff --git a/frontend/web/components/modals/payment/PaymentButton.tsx b/frontend/web/components/modals/payment/PaymentButton.tsx new file mode 100644 index 000000000000..c708d04f7fb3 --- /dev/null +++ b/frontend/web/components/modals/payment/PaymentButton.tsx @@ -0,0 +1,50 @@ +import React, { FC, ReactNode } from 'react' +import { usePaymentState } from './hooks' +import { useChargebeeCheckout } from './hooks' + +type PaymentButtonProps = { + 'data-cb-plan-id'?: string + className?: string + children?: ReactNode + isDisableAccount?: string +} + +export const PaymentButton: FC = (props) => { + const { hasActiveSubscription, organisation } = usePaymentState() + const { openCheckout } = useChargebeeCheckout({ + onSuccess: props.isDisableAccount + ? () => { + window.location.href = '/organisations' + } + : undefined, + organisationId: organisation?.id, + }) + + if (hasActiveSubscription) { + return ( + { + const planId = props['data-cb-plan-id'] + if (planId) { + openCheckout(planId) + } + }} + className={props.className} + href='#' + > + {props.children} + + ) + } + + return ( + + {props.children} + + ) +} diff --git a/frontend/web/components/modals/payment/index.tsx b/frontend/web/components/modals/payment/index.tsx new file mode 100644 index 000000000000..3ba510908b0a --- /dev/null +++ b/frontend/web/components/modals/payment/index.tsx @@ -0,0 +1,63 @@ +import React, { ComponentProps } from 'react' +// @ts-ignore +import makeAsyncScriptLoader from 'react-async-script' +import ConfigProvider from 'common/providers/ConfigProvider' +import Utils from 'common/utils/utils' +// @ts-ignore +import firstpromoter from 'project/firstPromoter' +import { Payment } from './Payment' + +type PaymentLoadParams = { + errored: boolean +} + +export const onPaymentLoad = ({ errored }: PaymentLoadParams) => { + if (errored) { + // TODO: no error details are available https://github.com/dozoisch/react-async-script/issues/58 + console.error('failed to load chargebee') + return + } + if (!Project.chargebee?.site) { + return + } + const planId = API.getCookie('plan') + let link: HTMLAnchorElement | undefined + + if (planId && Utils.getFlagsmithHasFeature('payments_enabled')) { + // Create a link element with data-cb-plan-id attribute + link = document.createElement('a') + link.setAttribute('data-cb-type', 'checkout') + link.setAttribute('data-cb-plan-id', planId) + link.setAttribute('href', 'javascript:void(0)') + // Append the link to the body + document.body.appendChild(link) + } + + Chargebee.init({ + site: Project.chargebee.site, + }) + Chargebee.registerAgain() + firstpromoter() + Chargebee.getInstance().setCheckoutCallbacks?.(() => ({ + success: (hostedPageId: string) => { + AppActions.updateSubscription(hostedPageId) + }, + })) + + if (link) { + link.click() + document.body.removeChild(link) + API.setCookie('plan', null) + } +} + +const WrappedPayment = makeAsyncScriptLoader( + 'https://js.chargebee.com/v2/chargebee.js', + { + removeOnUnmount: true, + }, +)(ConfigProvider(Payment)) + +export default (props: ComponentProps) => ( + +) From 35d9c0813d3b07d0ea05ed710a4420d755ec0982 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Wed, 18 Mar 2026 16:49:31 -0300 Subject: [PATCH 3/6] feat(payment): gate migration behind rtk_payment_modal_migration flag Wire up feature flag in BillingTab to switch between legacy and new Payment component. Clean up new components: - Replace inline styles with CSS classes in PricingPanel - Replace hardcoded icon colours with text-success/text-secondary - Use semantic button elements instead of anchor tags - Type organisation as Organisation | null - Add data-version='rtk' for DevTools identification Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web/components/modals/payment/Payment.tsx | 3 - .../modals/payment/PaymentButton.tsx | 45 ++++++------- .../modals/payment/PricingFeaturesList.tsx | 8 +-- .../modals/payment/PricingPanel.tsx | 63 +++++-------------- .../components/modals/payment/constants.ts | 3 - .../payment/hooks/useChargebeeCheckout.ts | 12 +--- .../modals/payment/hooks/usePaymentState.ts | 3 +- .../modals/payment/pricingFeatures.tsx | 19 +++--- .../web/components/modals/payment/types.ts | 2 +- .../organisation-settings/tabs/BillingTab.tsx | 9 ++- frontend/web/styles/project/_PricingPage.scss | 26 +++++--- 11 files changed, 83 insertions(+), 110 deletions(-) diff --git a/frontend/web/components/modals/payment/Payment.tsx b/frontend/web/components/modals/payment/Payment.tsx index aafdc8577129..b7fdd09c1d4e 100644 --- a/frontend/web/components/modals/payment/Payment.tsx +++ b/frontend/web/components/modals/payment/Payment.tsx @@ -67,7 +67,6 @@ export const Payment: FC = ({ = ({ = (props) => { +export const PaymentButton: FC = ({ + children, + className, + isDisableAccount, + ...rest +}) => { + const planId = rest['data-cb-plan-id'] const { hasActiveSubscription, organisation } = usePaymentState() - const { openCheckout } = useChargebeeCheckout({ - onSuccess: props.isDisableAccount + const { isLoading, openCheckout } = useChargebeeCheckout({ + onSuccess: isDisableAccount ? () => { window.location.href = '/organisations' } @@ -22,29 +27,25 @@ export const PaymentButton: FC = (props) => { if (hasActiveSubscription) { return ( - { - const planId = props['data-cb-plan-id'] - if (planId) { - openCheckout(planId) - } - }} - className={props.className} - href='#' + ) } return ( - - {props.children} - + {children} + ) } diff --git a/frontend/web/components/modals/payment/PricingFeaturesList.tsx b/frontend/web/components/modals/payment/PricingFeaturesList.tsx index 0360f134481e..c4eb752d733c 100644 --- a/frontend/web/components/modals/payment/PricingFeaturesList.tsx +++ b/frontend/web/components/modals/payment/PricingFeaturesList.tsx @@ -1,6 +1,5 @@ import React from 'react' import Icon from 'components/Icon' -import { PRIMARY_ICON_COLOR } from './constants' import { PricingFeature } from './types' export type PricingFeaturesListProps = { @@ -13,11 +12,8 @@ export const PricingFeaturesList = ({ features }: PricingFeaturesListProps) => { {features.map((feature, index) => (
  • - - + +
    {feature.text}
    diff --git a/frontend/web/components/modals/payment/PricingPanel.tsx b/frontend/web/components/modals/payment/PricingPanel.tsx index c08388e3617e..ba641946e135 100644 --- a/frontend/web/components/modals/payment/PricingPanel.tsx +++ b/frontend/web/components/modals/payment/PricingPanel.tsx @@ -1,6 +1,6 @@ import React, { ReactNode } from 'react' import classNames from 'classnames' -import Icon, { IconName } from 'components/Icon' +import Icon from 'components/Icon' import Button from 'components/base/forms/Button' import { PricingFeaturesList } from './PricingFeaturesList' import { PaymentButton } from './PaymentButton' @@ -9,8 +9,6 @@ import { PricingFeature } from './types' export type PricingPanelProps = { title: string - icon?: string - iconFill?: string priceMonthly?: string priceYearly?: string isYearly: boolean @@ -21,20 +19,16 @@ export type PricingPanelProps = { isDisableAccount?: string features: PricingFeature[] headerContent?: ReactNode - onContactSales?: () => void } export const PricingPanel = ({ chargebeePlanId, features, headerContent, - icon = 'flash', - iconFill, isDisableAccount, isEnterprise, isPurchased, isYearly, - onContactSales, priceMonthly, priceYearly, title, @@ -47,22 +41,9 @@ export const PricingPanel = ({ })} >
    -
    -
    -
    +
    +
    +
    {headerContent && (

    $

    {isYearly ? priceYearly : priceMonthly}{' '} -

    /mo
    + /mo )} @@ -107,32 +88,22 @@ export const PricingPanel = ({ )}
    -
    +
    -
    +
    {!viewOnly && !isEnterprise && chargebeePlanId && ( - <> - - {isPurchased ? 'Purchased' : '14 Day Free Trial'} - - + + {isPurchased ? 'Purchased' : '14 Day Free Trial'} + )} {!viewOnly && isEnterprise && (
    ) } diff --git a/frontend/web/styles/project/_PricingPage.scss b/frontend/web/styles/project/_PricingPage.scss index de9991446b89..4656a10469e8 100644 --- a/frontend/web/styles/project/_PricingPage.scss +++ b/frontend/web/styles/project/_PricingPage.scss @@ -1,5 +1,22 @@ -.panel-content.p-4 { - padding: unset !important; +.pricing-panel { + border-radius: 4px; +} + +.pricing-panel-content { + background-color: rgba(39, 171, 149, 0.08); + min-height: 320px; + border-radius: 4px; +} + +.pricing-panel-layout { + display: flex; + flex-direction: column; + height: 100%; + min-height: 200px; +} + +.pricing-panel-spacer { + flex: 1 1 auto; } .pricing-features > li { @@ -9,8 +26,3 @@ .pricing-features-item { flex-wrap: nowrap; } - -.pricing-panel, -.panel-content { - border-radius: 4px; -} From 77cc17e88cebfe309903a49bd84c35704e6e19b8 Mon Sep 17 00:00:00 2001 From: Talisson Costa Date: Fri, 3 Apr 2026 11:27:17 -0300 Subject: [PATCH 4/6] refactor(payment): remove store dependencies, pass data via props MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - usePaymentState accepts organisation as param, no more AccountStore - PaymentButton receives hasActiveSubscription and organisationId instead of the whole Organisation object - Remove dead viewOnly prop (always false, never true anywhere) - Props flow: BillingTab → Payment → PricingPanel → PaymentButton Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web/components/modals/payment/Payment.tsx | 37 ++++++++++++++++--- .../modals/payment/PaymentButton.tsx | 9 +++-- .../modals/payment/PricingPanel.tsx | 12 ++++-- .../modals/payment/hooks/usePaymentState.ts | 17 ++------- .../organisation-settings/tabs/BillingTab.tsx | 2 +- 5 files changed, 50 insertions(+), 27 deletions(-) diff --git a/frontend/web/components/modals/payment/Payment.tsx b/frontend/web/components/modals/payment/Payment.tsx index b7fdd09c1d4e..ce4be68eeb40 100644 --- a/frontend/web/components/modals/payment/Payment.tsx +++ b/frontend/web/components/modals/payment/Payment.tsx @@ -1,33 +1,48 @@ import React, { FC, useEffect } from 'react' import Constants from 'common/constants' +import Utils from 'common/utils/utils' import InfoMessage from 'components/InfoMessage' import BlockedOrgInfo from 'components/BlockedOrgInfo' +import { Organisation } from 'common/types/responses' import { PricingToggle } from './PricingToggle' import { PricingPanel } from './PricingPanel' import { startupFeatures, enterpriseFeatures } from './pricingFeatures' import { + CHARGEBEE_SCRIPT_URL, CONTACT_US_URL, ON_PREMISE_HOSTING_URL, SUPPORT_EMAIL, SUPPORT_EMAIL_URL, } from './constants' +import { useScript } from 'common/hooks/useScript' import { usePaymentState } from './hooks' +import { initChargebee } from './chargebee' -type PaymentProps = { - viewOnly?: boolean +export type PaymentProps = { isDisableAccountText?: string + organisation: Organisation } export const Payment: FC = ({ isDisableAccountText, - viewOnly, + organisation, }) => { - const { isAWS, plan, setYearly, yearly } = usePaymentState() + const { error, ready } = useScript(CHARGEBEE_SCRIPT_URL) + const { hasActiveSubscription, isAWS, plan, setYearly, yearly } = + usePaymentState({ organisation }) useEffect(() => { API.trackPage(Constants.modals.PAYMENT) }, []) + useEffect(() => { + if (ready && !error) { + initChargebee({ + paymentsEnabled: Utils.getFlagsmithHasFeature('payments_enabled'), + }) + } + }, [ready, error]) + if (isAWS) { return (
    @@ -41,6 +56,14 @@ export const Payment: FC = ({ ) } + if (!ready) { + return ( +
    + +
    + ) + } + return (
    @@ -70,7 +93,6 @@ export const Payment: FC = ({ priceYearly='40' priceMonthly='45' isYearly={yearly} - viewOnly={viewOnly} chargebeePlanId={ yearly ? Project.plans?.startup?.annual @@ -79,14 +101,17 @@ export const Payment: FC = ({ isPurchased={plan.includes('startup')} isDisableAccount={isDisableAccountText} features={startupFeatures} + hasActiveSubscription={hasActiveSubscription} + organisationId={organisation.id} /> Optional{' '} diff --git a/frontend/web/components/modals/payment/PaymentButton.tsx b/frontend/web/components/modals/payment/PaymentButton.tsx index 11e95a0401a0..a4b6562940b8 100644 --- a/frontend/web/components/modals/payment/PaymentButton.tsx +++ b/frontend/web/components/modals/payment/PaymentButton.tsx @@ -1,28 +1,31 @@ import React, { FC, ReactNode } from 'react' -import { usePaymentState, useChargebeeCheckout } from './hooks' +import { useChargebeeCheckout } from './hooks' type PaymentButtonProps = { 'data-cb-plan-id'?: string className?: string children?: ReactNode isDisableAccount?: string + hasActiveSubscription: boolean + organisationId: number } export const PaymentButton: FC = ({ children, className, + hasActiveSubscription, isDisableAccount, + organisationId, ...rest }) => { const planId = rest['data-cb-plan-id'] - const { hasActiveSubscription, organisation } = usePaymentState() const { isLoading, openCheckout } = useChargebeeCheckout({ onSuccess: isDisableAccount ? () => { window.location.href = '/organisations' } : undefined, - organisationId: organisation?.id, + organisationId, }) if (hasActiveSubscription) { diff --git a/frontend/web/components/modals/payment/PricingPanel.tsx b/frontend/web/components/modals/payment/PricingPanel.tsx index ba641946e135..871573aca5cc 100644 --- a/frontend/web/components/modals/payment/PricingPanel.tsx +++ b/frontend/web/components/modals/payment/PricingPanel.tsx @@ -12,27 +12,29 @@ export type PricingPanelProps = { priceMonthly?: string priceYearly?: string isYearly: boolean - viewOnly?: boolean chargebeePlanId?: string isPurchased?: boolean isEnterprise?: boolean isDisableAccount?: string features: PricingFeature[] headerContent?: ReactNode + hasActiveSubscription: boolean + organisationId: number } export const PricingPanel = ({ chargebeePlanId, features, + hasActiveSubscription, headerContent, isDisableAccount, isEnterprise, isPurchased, isYearly, + organisationId, priceMonthly, priceYearly, title, - viewOnly, }: PricingPanelProps) => { return (
    - {!viewOnly && !isEnterprise && chargebeePlanId && ( + {!isEnterprise && chargebeePlanId && ( {isPurchased ? 'Purchased' : '14 Day Free Trial'} )} - {!viewOnly && isEnterprise && ( + {isEnterprise && (