From f266889cbef0545ee39ea53dd655fcfc1bc74737 Mon Sep 17 00:00:00 2001 From: Rishabh Mishra Date: Wed, 24 Jan 2024 17:17:03 +0530 Subject: [PATCH] feat(sdk): show plan upgrade downgrade (#471) * chore: add weightage to plans and grouped plans * test: add test for `plan_group_id` * chore: show plan action based on weightage difference * chore: sort invoice based on date * chore: remove console log --- .../organization/billing/invoices/index.tsx | 4 +- .../plans/helpers/helpers.test.ts | 370 +++++++++++++++++- .../organization/plans/helpers/index.ts | 9 + .../components/organization/plans/index.tsx | 70 +++- sdks/js/packages/core/react/utils/index.ts | 24 ++ sdks/js/packages/core/src/types.ts | 2 + 6 files changed, 454 insertions(+), 25 deletions(-) diff --git a/sdks/js/packages/core/react/components/organization/billing/invoices/index.tsx b/sdks/js/packages/core/react/components/organization/billing/invoices/index.tsx index 33ae8426f..6e2716ad4 100644 --- a/sdks/js/packages/core/react/components/organization/billing/invoices/index.tsx +++ b/sdks/js/packages/core/react/components/organization/billing/invoices/index.tsx @@ -153,7 +153,9 @@ export default function Invoices({ ? [...new Array(3)].map((_, i) => ({ id: i.toString() })) - : invoices; + : invoices.sort((a, b) => + dayjs(a.effective_at).isAfter(b.effective_at) ? -1 : 1 + ); }, [invoices, isLoading]); useEffect(() => { diff --git a/sdks/js/packages/core/react/components/organization/plans/helpers/helpers.test.ts b/sdks/js/packages/core/react/components/organization/plans/helpers/helpers.test.ts index bef312030..984ca3f88 100644 --- a/sdks/js/packages/core/react/components/organization/plans/helpers/helpers.test.ts +++ b/sdks/js/packages/core/react/components/organization/plans/helpers/helpers.test.ts @@ -133,6 +133,7 @@ describe('Plans:helpers:groupPlansPricingByInterval', () => { slug: 'starter-plan-product-1-product-2', title: 'Starter Plan', description: 'Starter Plan', + weightage: 0, intervals: { year: { planId: 'plan-1', @@ -140,7 +141,8 @@ describe('Plans:helpers:groupPlansPricingByInterval', () => { amount: 0, behavior: '', currency: 'INR', - interval: 'year' + interval: 'year', + weightage: 0 }, month: { planId: 'plan-2', @@ -148,7 +150,8 @@ describe('Plans:helpers:groupPlansPricingByInterval', () => { amount: 0, behavior: '', currency: 'INR', - interval: 'month' + interval: 'month', + weightage: 0 } }, features: {} @@ -157,6 +160,7 @@ describe('Plans:helpers:groupPlansPricingByInterval', () => { slug: 'starter-plan-3-product-1-product-3', title: 'Starter Plan 3', description: 'Starter Plan 3', + weightage: 0, intervals: { month: { amount: 500, @@ -164,7 +168,367 @@ describe('Plans:helpers:groupPlansPricingByInterval', () => { currency: 'INR', planId: 'plan-3', planName: 'starter_plan_plan-3', - interval: 'month' + interval: 'month', + weightage: 0 + } + }, + features: {} + } + ]); + }); + + test('should add plans weightage', () => { + const plans: V1Beta1Plan[] = [ + { + id: 'plan-1', + name: 'starter_plan_plan-1', + title: 'Starter Plan', + description: 'Starter Plan', + interval: 'year', + products: [ + { + id: 'product-1', + prices: [ + { + amount: '0', + interval: 'year', + currency: 'INR' + }, + { + amount: '0', + interval: 'month', + currency: 'INR' + } + ] + }, + { + id: 'product-2', + prices: [ + { + amount: '0', + interval: 'year', + currency: 'INR' + }, + { + amount: '0', + interval: 'month', + currency: 'INR' + } + ] + } + ], + metadata: { + weightage: '1' + } + }, + { + id: 'plan-2', + name: 'starter_plan_plan-2', + title: 'Starter Plan', + description: 'Starter Plan', + interval: 'month', + products: [ + { + id: 'product-1', + prices: [ + { + amount: '0', + interval: 'year', + currency: 'INR' + }, + { + amount: '0', + interval: 'month', + currency: 'INR' + } + ] + }, + { + id: 'product-2', + prices: [ + { + amount: '0', + interval: 'year', + currency: 'INR' + }, + { + amount: '0', + interval: 'month', + currency: 'INR' + } + ] + } + ], + metadata: { + weightage: '2' + } + }, + { + id: 'plan-3', + name: 'starter_plan_plan-3', + title: 'Starter Plan 3', + description: 'Starter Plan 3', + interval: 'month', + products: [ + { + id: 'product-1', + prices: [ + { + amount: '0', + interval: 'year', + currency: 'INR' + }, + { + amount: '0', + interval: 'month', + currency: 'INR' + } + ] + }, + { + id: 'product-3', + prices: [ + { + amount: '100', + interval: 'year', + currency: 'INR' + }, + { + amount: '500', + interval: 'month', + currency: 'INR' + } + ] + } + ], + metadata: { + weightage: '5' + } + } + ]; + + const result = groupPlansPricingByInterval(plans); + expect(result).toEqual([ + { + slug: 'starter-plan-product-1-product-2', + title: 'Starter Plan', + description: 'Starter Plan', + weightage: 3, + intervals: { + year: { + planId: 'plan-1', + planName: 'starter_plan_plan-1', + amount: 0, + behavior: '', + currency: 'INR', + interval: 'year', + weightage: 1 + }, + month: { + planId: 'plan-2', + planName: 'starter_plan_plan-2', + amount: 0, + behavior: '', + currency: 'INR', + interval: 'month', + weightage: 2 + } + }, + features: {} + }, + { + slug: 'starter-plan-3-product-1-product-3', + title: 'Starter Plan 3', + description: 'Starter Plan 3', + weightage: 5, + intervals: { + month: { + amount: 500, + behavior: '', + currency: 'INR', + planId: 'plan-3', + planName: 'starter_plan_plan-3', + interval: 'month', + weightage: 5 + } + }, + features: {} + } + ]); + }); + + test('should group plans based on `plan_group_id`', () => { + const plans: V1Beta1Plan[] = [ + { + id: 'plan-1', + name: 'starter_plan_plan-1', + title: 'Starter Plan', + description: 'Starter Plan', + interval: 'year', + products: [ + { + id: 'product-1', + prices: [ + { + amount: '0', + interval: 'year', + currency: 'INR' + }, + { + amount: '0', + interval: 'month', + currency: 'INR' + } + ] + }, + { + id: 'product-2', + prices: [ + { + amount: '0', + interval: 'year', + currency: 'INR' + }, + { + amount: '0', + interval: 'month', + currency: 'INR' + } + ] + } + ], + metadata: { + weightage: '1', + plan_group_id: 'group-1' + } + }, + { + id: 'plan-2', + name: 'starter_plan_plan-2', + title: 'Starter Plan', + description: 'Starter Plan', + interval: 'month', + products: [ + { + id: 'product-1', + prices: [ + { + amount: '0', + interval: 'year', + currency: 'INR' + }, + { + amount: '0', + interval: 'month', + currency: 'INR' + } + ] + }, + { + id: 'product-2', + prices: [ + { + amount: '0', + interval: 'year', + currency: 'INR' + }, + { + amount: '0', + interval: 'month', + currency: 'INR' + } + ] + } + ], + metadata: { + weightage: '2', + plan_group_id: 'group-1' + } + }, + { + id: 'plan-3', + name: 'starter_plan_plan-3', + title: 'Starter Plan 3', + description: 'Starter Plan 3', + interval: 'week', + products: [ + { + id: 'product-1', + prices: [ + { + amount: '0', + interval: 'year', + currency: 'INR' + }, + { + amount: '0', + interval: 'month', + currency: 'INR' + } + ] + }, + { + id: 'product-3', + prices: [ + { + amount: '100', + interval: 'year', + currency: 'INR' + }, + { + amount: '500', + interval: 'month', + currency: 'INR' + }, + { + amount: '500', + interval: 'week', + currency: 'INR' + } + ] + } + ], + metadata: { + weightage: '5', + plan_group_id: 'group-1' + } + } + ]; + + const result = groupPlansPricingByInterval(plans); + expect(result).toEqual([ + { + slug: 'group-1', + title: 'Starter Plan', + description: 'Starter Plan', + weightage: 8, + intervals: { + year: { + planId: 'plan-1', + planName: 'starter_plan_plan-1', + amount: 0, + behavior: '', + currency: 'INR', + interval: 'year', + weightage: 1 + }, + month: { + planId: 'plan-2', + planName: 'starter_plan_plan-2', + amount: 0, + behavior: '', + currency: 'INR', + interval: 'month', + weightage: 2 + }, + week: { + amount: 500, + behavior: '', + currency: 'INR', + planId: 'plan-3', + planName: 'starter_plan_plan-3', + interval: 'week', + weightage: 5 } }, features: {} diff --git a/sdks/js/packages/core/react/components/organization/plans/helpers/index.ts b/sdks/js/packages/core/react/components/organization/plans/helpers/index.ts index 7cc2e0f91..cfbd7bb34 100644 --- a/sdks/js/packages/core/react/components/organization/plans/helpers/index.ts +++ b/sdks/js/packages/core/react/components/organization/plans/helpers/index.ts @@ -24,6 +24,7 @@ export function groupPlansPricingByInterval(plans: V1Beta1Plan[]) { slug: slug, title: plan.title, description: plan?.description, + weightage: 0, intervals: {}, features: {} }; @@ -45,12 +46,20 @@ export function groupPlansPricingByInterval(plans: V1Beta1Plan[]) { plansMap[slug].features[feature?.id || ''] = feature; }); }, {} as IntervalPricing) || ({} as IntervalPricing); + + const planMetadata = (plan?.metadata as Record) || {}; plansMap[slug].intervals[planInterval] = { planId: plan?.id || '', planName: plan?.name || '', interval: planInterval, + weightage: planMetadata?.weightage ? Number(planMetadata?.weightage) : 0, ...productPrices }; + + plansMap[slug].weightage = Object.values(plansMap[slug].intervals).reduce( + (acc, data) => acc + data.weightage, + 0 + ); }); return Object.values(plansMap); diff --git a/sdks/js/packages/core/react/components/organization/plans/index.tsx b/sdks/js/packages/core/react/components/organization/plans/index.tsx index 17c9bfc15..db90a086e 100644 --- a/sdks/js/packages/core/react/components/organization/plans/index.tsx +++ b/sdks/js/packages/core/react/components/organization/plans/index.tsx @@ -17,10 +17,12 @@ import { getAllPlansFeatuesMap, groupPlansPricingByInterval } from './helpers'; import { IntervalKeys, IntervalLabelMap, + IntervalPricingWithPlan, PlanIntervalPricing } from '~/src/types'; import checkCircle from '~/react/assets/check-circle.svg'; import qs from 'query-string'; +import { getPlanChangeAction } from '~/react/utils'; const PlansLoader = () => { return ( @@ -77,26 +79,25 @@ const PlansHeader = ({ billingSupportEmail }: PlansHeaderProps) => { const PlanPricingColumn = ({ plan, - featureMap = {} + featureMap = {}, + currentPlan }: { plan: PlanIntervalPricing; featureMap: Record; + currentPlan?: IntervalPricingWithPlan; }) => { - const { - client, - activeOrganization, - billingAccount, - config, - activeSubscription - } = useFrontier(); + const { client, activeOrganization, billingAccount, config } = useFrontier(); const [isLoading, setIsLoading] = useState(false); - const planIntervals = (Object.keys(plan.intervals).sort() || - []) as IntervalKeys[]; + const planIntervals = + Object.values(plan.intervals) + .sort((a, b) => a.weightage - b.weightage) + .map(i => i.interval) || []; + const [selectedInterval, setSelectedInterval] = useState(() => { const activePlan = Object.values(plan?.intervals).find( - p => p.planId === activeSubscription?.plan_id + p => p.planId === currentPlan?.planId ); return activePlan?.interval || planIntervals[0]; }); @@ -110,17 +111,23 @@ const PlanPricingColumn = ({ const selectedIntervalPricing = plan.intervals[selectedInterval]; const action = useMemo(() => { - if (selectedIntervalPricing.planId === activeSubscription?.plan_id) { + if (selectedIntervalPricing.planId === currentPlan?.planId) { return { disabled: true, - text: 'Current Plan' + btnLabel: 'Current Plan', + btnLoadingLabel: 'Current Plan' }; } + + const planAction = getPlanChangeAction( + selectedIntervalPricing, + currentPlan + ); return { disabled: false, - text: 'Upgrade' + ...planAction }; - }, [activeSubscription?.plan_id, selectedIntervalPricing.planId]); + }, [currentPlan, selectedIntervalPricing]); const onPlanActionClick = useCallback(async () => { setIsLoading(true); @@ -184,7 +191,7 @@ const PlanPricingColumn = ({ {selectedIntervalPricing.currency}{' '} - {selectedIntervalPricing.amount?.toString()} + {selectedIntervalPricing.amount.toString()} per seat/{selectedInterval} @@ -201,7 +208,7 @@ const PlanPricingColumn = ({ onClick={onPlanActionClick} disabled={action?.disabled || isLoading} > - {isLoading ? 'Upgrading...' : action.text} + {isLoading ? `${action.btnLoadingLabel}....` : action.btnLabel} {planIntervals.length > 1 ? ( { +const PlansList = ({ plans = [], currentPlanId }: PlansListProps) => { if (plans.length === 0) return ; - const groupedPlans = groupPlansPricingByInterval(plans); + const groupedPlans = groupPlansPricingByInterval(plans).sort( + (a, b) => a.weightage - b.weightage + ); const featuresMap = getAllPlansFeatuesMap(plans); + + let currentPlanPricing: IntervalPricingWithPlan | undefined; + groupedPlans.forEach(group => { + Object.values(group.intervals).forEach(plan => { + if (plan.planId === currentPlanId) { + currentPlanPricing = plan; + } + }); + }); + return ( @@ -305,6 +325,7 @@ const PlansList = ({ plans = [] }: PlansListProps) => { plan={plan} key={plan.slug} featureMap={featuresMap} + currentPlan={currentPlanPricing} /> ))} @@ -314,7 +335,7 @@ const PlansList = ({ plans = [] }: PlansListProps) => { }; export default function Plans() { - const { config, client } = useFrontier(); + const { config, client, activeSubscription } = useFrontier(); const [isPlansLoading, setIsPlansLoading] = useState(false); const [plans, setPlans] = useState([]); @@ -348,7 +369,14 @@ export default function Plans() { - {isPlansLoading ? : } + {isPlansLoading ? ( + + ) : ( + + )} ); diff --git a/sdks/js/packages/core/react/utils/index.ts b/sdks/js/packages/core/react/utils/index.ts index b21880dfb..3020e1b05 100644 --- a/sdks/js/packages/core/react/utils/index.ts +++ b/sdks/js/packages/core/react/utils/index.ts @@ -1,5 +1,6 @@ import dayjs from 'dayjs'; import { V1Beta1Subscription, BillingAccountAddress } from '~/src'; +import { IntervalPricingWithPlan } from '~/src/types'; export const AuthTooltipMessage = 'You don’t have access to perform this action'; @@ -25,3 +26,26 @@ export const getActiveSubscription = (subscriptions: V1Beta1Subscription[]) => { return activeSubscriptions[0]; }; + +export const getPlanChangeAction = ( + nextPlan: IntervalPricingWithPlan, + currentPlan?: IntervalPricingWithPlan +) => { + const diff = nextPlan.weightage - (currentPlan?.weightage || 0); + if (diff > 0) { + return { + btnLabel: 'Upgrade', + btnLoadingLabel: 'Upgrading' + }; + } else if (diff < 0) { + return { + btnLabel: 'Downgrade', + btnLoadingLabel: 'Downgrading' + }; + } else { + return { + btnLabel: 'Change', + btnLoadingLabel: 'Changing' + }; + } +}; diff --git a/sdks/js/packages/core/src/types.ts b/sdks/js/packages/core/src/types.ts index ddb443bdd..399aaa6f1 100644 --- a/sdks/js/packages/core/src/types.ts +++ b/sdks/js/packages/core/src/types.ts @@ -82,6 +82,7 @@ export interface IntervalPricingWithPlan extends IntervalPricing { planId: string; planName: string; interval: IntervalKeys; + weightage: number; } export interface PlanIntervalPricing { @@ -89,5 +90,6 @@ export interface PlanIntervalPricing { title: string; description: string; intervals: Record; + weightage: number; features: Record; }