diff --git a/components/src/components/elements/pricing-table/PricingTable.tsx b/components/src/components/elements/pricing-table/PricingTable.tsx index 2407971f..8abe9ff5 100644 --- a/components/src/components/elements/pricing-table/PricingTable.tsx +++ b/components/src/components/elements/pricing-table/PricingTable.tsx @@ -10,7 +10,11 @@ import { useTranslation } from "react-i18next"; import { type CompanyPlanDetailResponseData } from "../../../api/checkoutexternal"; import { type PlanViewPublicResponseData } from "../../../api/componentspublic"; -import { TEXT_BASE_SIZE, VISIBLE_ENTITLEMENT_COUNT } from "../../../const"; +import { + PriceInterval, + TEXT_BASE_SIZE, + VISIBLE_ENTITLEMENT_COUNT, +} from "../../../const"; import { type FontStyle } from "../../../context"; import { useAvailablePlans, useEmbed } from "../../../hooks"; import type { DeepPartial, ElementProps } from "../../../types"; @@ -131,7 +135,7 @@ export const PricingTable = forwardRef< const { data, settings, isPending, hydratePublic } = useEmbed(); - const { currentPeriod, isStandalone } = useMemo(() => { + const { currentPeriod, showPeriodToggle, isStandalone } = useMemo(() => { if (isCheckoutData(data)) { const billingSubscription = data.company?.billingSubscription; const isTrialSubscription = billingSubscription?.status === "trialing"; @@ -141,6 +145,7 @@ export const PricingTable = forwardRef< currentPeriod: data.company?.plan?.planPeriod || "month", currentAddOns: data.company?.addOns || [], canCheckout: data.capabilities?.checkout ?? true, + showPeriodToggle: data.showPeriodToggle ?? props.showPeriodToggle, isTrialSubscription, willSubscriptionCancel, isStandalone: false, @@ -151,15 +156,18 @@ export const PricingTable = forwardRef< currentPeriod: "month", currentAddOns: [], canCheckout: true, + showPeriodToggle: props.showPeriodToggle, isTrialSubscription: false, willSubscriptionCancel: false, isStandalone: true, }; - }, [data]); + }, [props.showPeriodToggle, data]); const [selectedPeriod, setSelectedPeriod] = useState(currentPeriod); - const { plans, addOns, periods } = useAvailablePlans(selectedPeriod); + const { plans, addOns, periods } = useAvailablePlans(selectedPeriod, { + useSelectedPeriod: showPeriodToggle, + }); const [entitlementCounts, setEntitlementCounts] = useState(() => plans.reduce(entitlementCountsReducer, {}), @@ -245,7 +253,7 @@ export const PricingTable = forwardRef< t("Plans")} - {props.showPeriodToggle && periods.length > 1 && ( + {showPeriodToggle && periods.length > 1 && ( - {plans.map((plan, index, self) => ( - - ))} + {plans.map((plan, index, self) => { + const planPeriod = showPeriodToggle + ? selectedPeriod + : plan.yearlyPrice && !plan.monthlyPrice + ? PriceInterval.Year + : PriceInterval.Month; + + return ( + + ); + })} )} @@ -308,19 +324,27 @@ export const PricingTable = forwardRef< $gridTemplateColumns="repeat(auto-fill, minmax(320px, 1fr))" $gap="1rem" > - {addOns.map((addOn, index) => ( - - ))} + {addOns.map((addOn, index) => { + const addOnPeriod = showPeriodToggle + ? selectedPeriod + : addOn.yearlyPrice && !addOn.monthlyPrice + ? PriceInterval.Year + : PriceInterval.Month; + + return ( + + ); + })} )} diff --git a/components/src/components/shared/checkout-dialog/CheckoutDialog.tsx b/components/src/components/shared/checkout-dialog/CheckoutDialog.tsx index 05910f73..a99c09b6 100644 --- a/components/src/components/shared/checkout-dialog/CheckoutDialog.tsx +++ b/components/src/components/shared/checkout-dialog/CheckoutDialog.tsx @@ -124,24 +124,30 @@ export const CheckoutDialog = ({ top = 0 }: CheckoutDialogProps) => { periods: availablePeriods, } = useAvailablePlans(planPeriod); - const { currentPlanId, currentEntitlements, trialPaymentMethodRequired } = - useMemo(() => { - if (isCheckoutData(data)) { - return { - currentPlanId: data.company?.plan?.id, - currentEntitlements: data.featureUsage - ? data.featureUsage.features - : [], - trialPaymentMethodRequired: data.trialPaymentMethodRequired === true, - }; - } - + const { + currentPlanId, + currentEntitlements, + showPeriodToggle, + trialPaymentMethodRequired, + } = useMemo(() => { + if (isCheckoutData(data)) { return { - currentPlanId: undefined, - currentEntitlements: [], - trialPaymentMethodRequired: false, + currentPlanId: data.company?.plan?.id, + currentEntitlements: data.featureUsage + ? data.featureUsage.features + : [], + showPeriodToggle: data.showPeriodToggle, + trialPaymentMethodRequired: data.trialPaymentMethodRequired === true, }; - }, [data]); + } + + return { + currentPlanId: undefined, + currentEntitlements: [], + showPeriodToggle: true, + trialPaymentMethodRequired: false, + }; + }, [data]); const [selectedPlan, setSelectedPlan] = useState( () => { @@ -867,14 +873,16 @@ export const CheckoutDialog = ({ top = 0 }: CheckoutDialogProps) => { )} - {checkoutStage === "plan" && availablePeriods.length > 1 && ( - - )} + {checkoutStage === "plan" && + showPeriodToggle && + availablePeriods.length > 1 && ( + + )} {checkoutStage === "plan" && ( @@ -885,6 +893,7 @@ export const CheckoutDialog = ({ top = 0 }: CheckoutDialogProps) => { selectedPlan={selectedPlan} selectPlan={selectPlan} shouldTrial={shouldTrial} + showPeriodToggle={showPeriodToggle} /> )} diff --git a/components/src/components/shared/checkout-dialog/Plan.tsx b/components/src/components/shared/checkout-dialog/Plan.tsx index f8fb553b..778b12f5 100644 --- a/components/src/components/shared/checkout-dialog/Plan.tsx +++ b/components/src/components/shared/checkout-dialog/Plan.tsx @@ -5,6 +5,7 @@ import { EntitlementValueType, FeatureType, PriceBehavior, + PriceInterval, TEXT_BASE_SIZE, VISIBLE_ENTITLEMENT_COUNT, } from "../../../const"; @@ -252,6 +253,7 @@ interface PlanProps { shouldTrial?: boolean; }) => void; shouldTrial: boolean; + showPeriodToggle: boolean; } export const Plan = ({ @@ -261,6 +263,7 @@ export const Plan = ({ period, selectPlan, shouldTrial, + showPeriodToggle, }: PlanProps) => { const { t } = useTranslation(); @@ -312,8 +315,13 @@ export const Plan = ({ $flexGrow={1} > {plans.map((plan, planIndex) => { + const planPeriod = showPeriodToggle + ? period + : plan.yearlyPrice && !plan.monthlyPrice + ? PriceInterval.Year + : PriceInterval.Month; const { price: planPrice, currency: planCurrency } = - getPlanPrice(plan, period) || {}; + getPlanPrice(plan, planPeriod) || {}; const credits = groupPlanCreditGrants(plan.includedCreditGrants); const hasUsageBasedEntitlements = plan.entitlements.some( (entitlement) => !!entitlement.priceBehavior, @@ -395,7 +403,7 @@ export const Plan = ({ (16 / 30) * settings.theme.typography.heading2.fontSize } > - /{period} + /{planPeriod} )} @@ -502,7 +510,7 @@ export const Plan = ({ priceTier: entitlementPriceTiers, currency: entitlementCurrency, packageSize: entitlementPackageSize = 1, - } = getEntitlementPrice(entitlement, period) || {}; + } = getEntitlementPrice(entitlement, planPeriod) || {}; const metricPeriodName = getMetricPeriodName(entitlement); @@ -557,7 +565,7 @@ export const Plan = ({ PriceBehavior.PayInAdvance && ( <> {" "} - {t("per")} {period} + {t("per")} {planPeriod} )} @@ -565,7 +573,7 @@ export const Plan = ({ PriceBehavior.Tiered ? ( ) : entitlement.priceBehavior === PriceBehavior.Credit && @@ -640,7 +648,7 @@ export const Plan = ({ )} {entitlement.feature.featureType === FeatureType.Trait && ( - <>/{shortenPeriod(period)} + <>/{shortenPeriod(planPeriod)} )} ) : ( @@ -649,7 +657,7 @@ export const Plan = ({ diff --git a/components/src/const/api.ts b/components/src/const/api.ts index 223ff018..65bbdf90 100644 --- a/components/src/const/api.ts +++ b/components/src/const/api.ts @@ -1,3 +1,10 @@ +export enum PriceInterval { + OneTime = "one-time", + Day = "day", + Month = "month", + Year = "year", +} + export enum PriceBehavior { PayInAdvance = "pay_in_advance", PayAsYouGo = "pay_as_you_go", diff --git a/components/src/hooks/useAvailablePlans.ts b/components/src/hooks/useAvailablePlans.ts index b7041d22..942de6d7 100644 --- a/components/src/hooks/useAvailablePlans.ts +++ b/components/src/hooks/useAvailablePlans.ts @@ -7,7 +7,14 @@ import { ChargeType } from "../utils"; import { useEmbed } from "."; -export function useAvailablePlans(activePeriod: string) { +interface AvailablePlanOptions { + useSelectedPeriod?: boolean; +} + +export function useAvailablePlans( + activePeriod: string, + options: AvailablePlanOptions = { useSelectedPeriod: true }, +) { const { data, settings } = useEmbed(); const getAvailablePeriods = useCallback((): string[] => { @@ -35,16 +42,25 @@ export function useAvailablePlans(activePeriod: string) { const activePlans = settings.mode === "edit" ? plans.slice() - : plans.filter( - (plan) => - (activePeriod === "month" && plan.monthlyPrice) || - (activePeriod === "year" && plan.yearlyPrice) || - plan.chargeType === ChargeType.oneTime, - ); + : plans.filter((plan) => { + if (options.useSelectedPeriod) { + return ( + (activePeriod === "month" && plan.monthlyPrice) || + (activePeriod === "year" && plan.yearlyPrice) || + plan.chargeType === ChargeType.oneTime + ); + } + + return ( + plan.monthlyPrice || + plan.yearlyPrice || + plan.chargeType === ChargeType.oneTime + ); + }); return activePlans.map((plan) => ({ ...plan, isSelected: false })); }, - [activePeriod, settings.mode], + [activePeriod, options.useSelectedPeriod, settings.mode], ); return useMemo(() => { diff --git a/components/src/utils/api/billing.ts b/components/src/utils/api/billing.ts index 8ea1740d..dbc6c8f7 100644 --- a/components/src/utils/api/billing.ts +++ b/components/src/utils/api/billing.ts @@ -21,11 +21,22 @@ export function getPriceValue(billingPrice: BillingPrice): number { return price; } +interface PlanPriceOptions { + useSelectedPeriod?: boolean; +} + export function getPlanPrice( plan: Plan, period = "month", + options: PlanPriceOptions = { useSelectedPeriod: true }, ): BillingPriceResponseData | undefined { - const billingPrice = period === "year" ? plan.yearlyPrice : plan.monthlyPrice; + const billingPrice = options.useSelectedPeriod + ? period === "year" + ? plan.yearlyPrice + : plan.monthlyPrice + : plan.yearlyPrice && !plan.monthlyPrice + ? plan.yearlyPrice + : plan.monthlyPrice; if (billingPrice) { return { ...billingPrice, price: getPriceValue(billingPrice) };