Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import { useNavigate, useParams } from '@tanstack/react-router';
import { useFrontier } from '~/react/contexts/FrontierContext';
import { getPlanIntervalName, getPlanPrice } from '~/react/utils';
import * as _ from 'lodash';
import { usePlans } from '../../plans/hooks/usePlans';
import { DEFAULT_DATE_FORMAT } from '~/react/utils/constants';
import cross from '~/react/assets/cross.svg';
import styles from '../../organization.module.css';
import { timestampToDayjs } from '~/utils/timestamp';
import { usePlans } from '~/react/views/plans/hooks/usePlans';

export function ConfirmCycleSwitch() {
const {
Expand Down Expand Up @@ -61,7 +61,7 @@ export function ConfirmCycleSwitch() {

const isUpgrade =
(Number(nextPlanMetadata?.weightage) || 0) -
(Number(activePlanMetadata?.weightage) || 0) >
(Number(activePlanMetadata?.weightage) || 0) >
0;

const isLoading = isAllPlansLoading;
Expand Down Expand Up @@ -100,8 +100,8 @@ export function ConfirmCycleSwitch() {

const cycleSwitchDate = activeSubscription?.currentPeriodEndAt
? timestampToDayjs(activeSubscription?.currentPeriodEndAt)?.format(
config?.dateFormat || DEFAULT_DATE_FORMAT
)
config?.dateFormat || DEFAULT_DATE_FORMAT
)
: 'the next billing cycle';

return (
Expand Down
188 changes: 3 additions & 185 deletions web/sdk/react/components/organization/plans/index.tsx
Original file line number Diff line number Diff line change
@@ -1,189 +1,7 @@
import { EmptyState, Skeleton, Text, Flex } from '@raystack/apsara';
import { Outlet } from '@tanstack/react-router';
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
import { useFrontier } from '~/react/contexts/FrontierContext';
import { groupPlansPricingByInterval } from './helpers';
import { IntervalPricingWithPlan } from '~/src/types';
import { UpcomingPlanChangeBanner } from '~/react/components/common/upcoming-plan-change-banner';
import { PlansHeader } from './header';
import { PlanPricingColumn } from './pricing-column';
import { useBillingPermission } from '~/react/hooks/useBillingPermission';
import { useQuery as useConnectQuery } from '@connectrpc/connect-query';
import { FrontierServiceQueries } from '~hooks';
import { create } from '@bufbuild/protobuf';
import { Feature, ListFeaturesRequestSchema } from '@raystack/proton/frontier';
import { Plan } from '@raystack/proton/frontier';
import sharedStyles from '../styles.module.css';
import plansStyles from './plans.module.css';
'use client';

const PlansLoader = () => {
return (
<Flex direction="column" gap={4}>
{[...new Array(2)].map((_, i) => (
<Skeleton containerClassName={plansStyles.flex1} key={`loader-${i}`} />
))}
</Flex>
);
};

const NoPlans = () => {
return (
<EmptyState
icon={<ExclamationTriangleIcon />}
heading={<span style={{ fontWeight: 'bold' }}>No Plans Available</span>}
subHeading={
'Sorry, No plans available at this moment. Please try again later'
}
/>
);
};

interface PlansListProps {
plans: Plan[];
currentPlanId: string;
allowAction: boolean;
features: Feature[];
}

const PlansList = ({
plans = [],
features = [],
currentPlanId,
allowAction
}: PlansListProps) => {
if (plans.length === 0) return <NoPlans />;

const groupedPlans = groupPlansPricingByInterval(plans).sort(
(a, b) => a.weightage - b.weightage
);

let currentPlanPricing: IntervalPricingWithPlan | undefined;
groupedPlans.forEach(group => {
Object.values(group.intervals).forEach(plan => {
if (plan.planId === currentPlanId) {
currentPlanPricing = plan;
}
});
});

const totalFeatures = features.length;

const featureTitleMap = features.reduce((acc, f) => {
const weightage =
(f.metadata as Record<string, any>)?.weightage || totalFeatures;
acc[f.title || ''] = weightage;
return acc;
}, {} as Record<string, number>);

const sortedFeatures = Object.entries(featureTitleMap)
.sort((f1, f2) => f1[1] - f2[1])
.map(f => f[0])
.filter(f => Boolean(f));

return (
<Flex>
<Flex style={{ overflow: 'hidden', flex: 1 }}>
<div className={plansStyles.leftPanel}>
<div className={plansStyles.planInfoColumn}>{''}</div>
<Flex direction="column">
<Flex
align="center"
justify="start"
className={plansStyles.featureCell}
>
<Text size="small" className={plansStyles.featureTableHeading}>
Features
</Text>
</Flex>
{sortedFeatures.map(feature => {
return (
<Flex
key={feature}
align="center"
justify="start"
className={plansStyles.featureCell}
>
<Text size={3} className={plansStyles.featureLabel}>
{feature}
</Text>
</Flex>
);
})}
</Flex>
</div>
<Flex className={plansStyles.rightPanel}>
{groupedPlans.map(plan => (
<PlanPricingColumn
plan={plan}
key={plan.slug}
features={sortedFeatures}
currentPlan={currentPlanPricing}
allowAction={allowAction}
/>
))}
</Flex>
</Flex>
</Flex>
);
};
import { PlansPage } from '~/react/views/plans';

export default function Plans() {
const {
config,
activeSubscription,
isActiveSubscriptionLoading,
isActiveOrganizationLoading,
basePlan,
allPlans,
isAllPlansLoading
} = useFrontier();

const { isFetching: isPermissionsFetching, isAllowed: canChangePlan } =
useBillingPermission();

const { data: featuresData } = useConnectQuery(
FrontierServiceQueries.listFeatures,
create(ListFeaturesRequestSchema, {})
);

const features = (featuresData?.features || []) as Feature[];

const plans = [...(basePlan ? [basePlan] : []), ...allPlans];

const isLoading =
isAllPlansLoading ||
isPermissionsFetching ||
isActiveSubscriptionLoading ||
isActiveOrganizationLoading;

return (
<Flex direction="column" style={{ width: '100%', overflow: 'hidden' }}>
<Flex direction="column" className={sharedStyles.container}>
<Flex direction="row" justify="between" align="center" className={sharedStyles.header}>
<PlansHeader
billingSupportEmail={config.billing?.supportEmail}
isLoading={isLoading}
/>
</Flex>
<Flex direction="column" gap={7}>
<UpcomingPlanChangeBanner
isLoading={isLoading}
subscription={activeSubscription}
isAllowed={canChangePlan}
/>
{isLoading ? (
<PlansLoader />
) : (
<PlansList
plans={plans}
features={features}
currentPlanId={activeSubscription?.planId || ''}
allowAction={canChangePlan}
/>
)}
</Flex>
</Flex>
<Outlet />
</Flex>
);
return <PlansPage />;
}
8 changes: 1 addition & 7 deletions web/sdk/react/components/organization/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import Tokens from './tokens';
import { AddTokens } from './tokens/add-tokens';
import { ConfirmCycleSwitch } from './billing/cycle-switch';
import Plans from './plans';
import ConfirmPlanChange from './plans/confirm-change';
import APIKeys from './api-keys';
import { AddServiceAccount } from './api-keys/add';
import ServiceUserPage from './api-keys/service-user';
Expand Down Expand Up @@ -229,11 +228,6 @@ const plansRoute = createRoute({
component: Plans
});

const planDowngradeRoute = createRoute({
getParentRoute: () => plansRoute,
path: '/confirm-change/$planId',
component: ConfirmPlanChange
});

const tokensRoute = createRoute({
getParentRoute: () => rootRoute,
Expand Down Expand Up @@ -320,7 +314,7 @@ export function getRootTree({ customScreens = [] }: getRootTreeOptions) {
profileRoute,
preferencesRoute,
billingRoute.addChildren([switchBillingCycleModalRoute]),
plansRoute.addChildren([planDowngradeRoute]),
plansRoute,
tokensRoute.addChildren([addTokensRoute]),
apiKeysRoute.addChildren([
addServiceAccountRoute,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,32 @@ import {
Flex,
Dialog
} from '@raystack/apsara';
import { useNavigate, useParams } from '@tanstack/react-router';
import * as _ from 'lodash';
import { useFrontier } from '~/react/contexts/FrontierContext';
import {
DEFAULT_DATE_FORMAT,
DEFAULT_PLAN_UPGRADE_MESSAGE
} from '~/react/utils/constants';
import { getPlanChangeAction, getPlanNameWithInterval } from '~/react/utils';
import planStyles from '../plans.module.css';
import { usePlans } from '../hooks/usePlans';
import planStyles from './plans.module.css';
import { usePlans } from './hooks/usePlans';
import cross from '~/react/assets/cross.svg';
import styles from '../../organization.module.css';
import orgStyles from '../../components/organization/organization.module.css';
import { useMessages } from '~/react/hooks/useMessages';
import { timestampToDayjs } from '~/utils/timestamp';
import { Plan } from '@raystack/proton/frontier';

export default function ConfirmPlanChange() {
const navigate = useNavigate({ from: '/plans/confirm-change/$planId' });
const { planId } = useParams({ from: '/plans/confirm-change/$planId' });
export interface ConfirmPlanChangeDialogProps {
open: boolean;
onOpenChange?: (value: boolean) => void;
planId: string;
}

export function ConfirmPlanChangeDialog({
open,
onOpenChange,
planId
}: ConfirmPlanChangeDialogProps) {
const {
activePlan,
isAllPlansLoading,
Expand Down Expand Up @@ -63,7 +70,7 @@ export default function ConfirmPlanChange() {
Number(activePlanMetadata?.weightage)
);

const cancel = useCallback(() => navigate({ to: '/plans' }), [navigate]);
const handleClose = useCallback(() => onOpenChange?.(false), [onOpenChange]);
const newPlanSlug = isNewPlanBasePlan ? 'base' : newPlan?.name;

const planChangeSlug = activePlan?.name
Expand All @@ -88,10 +95,10 @@ export default function ConfirmPlanChange() {
toast.success(`Plan ${actionName} successful`, {
description: `Your plan will ${actionName} on ${changeDate}`
});
cancel();
handleClose();
}
}, [
cancel,
handleClose,
config?.dateFormat,
planAction?.btnLabel,
planId,
Expand Down Expand Up @@ -139,10 +146,10 @@ export default function ConfirmPlanChange() {
}, [isNewPlanBasePlan, basePlan, currentPlan]);

useEffect(() => {
if (planId) {
if (planId && open) {
getPlan();
}
}, [getPlan, planId]);
}, [getPlan, planId, open]);

const isLoading = isAllPlansLoading || isNewPlanLoading;

Expand All @@ -161,10 +168,10 @@ export default function ConfirmPlanChange() {
: 'the next billing cycle';

return (
<Dialog open={true}>
<Dialog open={open} onOpenChange={onOpenChange}>
<Dialog.Content
style={{ padding: 0, maxWidth: '600px', width: '100%' }}
overlayClassName={styles.overlay}
overlayClassName={orgStyles.overlay}
>
<Dialog.Header>
<Flex justify="between" align="center" style={{ width: '100%' }}>
Expand All @@ -180,7 +187,7 @@ export default function ConfirmPlanChange() {
alt="cross"
style={{ cursor: 'pointer' }}
src={cross as unknown as string}
onClick={cancel}
onClick={handleClose}
data-test-id="frontier-sdk-confirm-plan-change-close-button"
/>
</Flex>
Expand Down Expand Up @@ -232,7 +239,7 @@ export default function ConfirmPlanChange() {
<Button
variant="outline"
color="neutral"
onClick={cancel}
onClick={handleClose}
data-test-id="frontier-sdk-confirm-plan-change-cancel-button"
>
Cancel
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
IntervalKeys,
IntervalPricing,
IntervalPricingWithPlan,
PlanIntervalPricing
} from '~/src/types';
import { getPlanPrice, makePlanSlug } from '~/react/utils';
Expand Down
5 changes: 5 additions & 0 deletions web/sdk/react/views/plans/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { default as PlansPage } from './plans-page';
export type { PlansPageProps } from './plans-page';

export { ConfirmPlanChangeDialog } from './confirm-plan-change-dialog';
export type { ConfirmPlanChangeDialogProps } from './confirm-plan-change-dialog';
Loading
Loading