Skip to content

Commit

Permalink
implement the trial summary popup
Browse files Browse the repository at this point in the history
  • Loading branch information
nilscox committed Jan 10, 2025
1 parent 858c06f commit 8f65a94
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 12 deletions.
5 changes: 3 additions & 2 deletions src/components/plan-icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { SvgComponent, SvgProps } from 'src/application/types';

import { IconCrown, IconGem, IconRocket } from './icons';

export function PlanIcon({ plan, ...props }: { plan: OrganizationPlan } & SvgProps) {
const Icon = map[plan];
export function PlanIcon({ plan, ...props }: { plan?: OrganizationPlan } & SvgProps) {
const Icon = map[plan!];

if (Icon) {
return <Icon {...props} />;
Expand All @@ -15,6 +15,7 @@ export function PlanIcon({ plan, ...props }: { plan: OrganizationPlan } & SvgPro

const map: Partial<Record<OrganizationPlan, SvgComponent>> = {
starter: IconRocket,
startup: IconGem,
pro: IconGem,
scale: IconCrown,
};
10 changes: 8 additions & 2 deletions src/hooks/feature-flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ import { useFeatureFlagEnabled } from 'posthog-js/react';
import { useMemo } from 'react';
import { z } from 'zod';

export function FeatureFlag({ feature, children }: { feature: string; children: React.ReactNode }) {
type FeatureFlagProps = {
feature: string;
fallback?: React.ReactNode;
children: React.ReactNode;
};

export function FeatureFlag({ feature, fallback = null, children }: FeatureFlagProps) {
const enabled = useFeatureFlag(feature);

if (!enabled) {
return null;
return fallback;
}

return children;
Expand Down
18 changes: 18 additions & 0 deletions src/intl/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1283,6 +1283,14 @@
"banner": {
"content": "Your trial is active for the next <strong>{days, plural, =1 {{days} day} other {{days} days}}</strong> or until your <strong>credit runs out</strong>. Enjoy exploring or <upgrade>upgrade to Pro</upgrade>!"
},
"summaryPopup": {
"currentPlan": "{plan} Plan",
"badge": "Trial",
"usage": "Usage",
"creditLeft": "Trial credit left",
"timeLeft": "{days, plural, =1 {{days} day} other {{days} days}} of trial left",
"cta": "Go to billing"
},
"ended": {
"planItem": {
"upgrade": "Upgrade to {plan} Plan",
Expand Down Expand Up @@ -1376,6 +1384,16 @@
"system": "System",
"logout": "Log out"
},
"organizationPlan": {
"currentPlan": "{plan} Plan",
"upgrade": "Upgrade now",
"free": "Free",
"upsell": {
"title": "Get the full experience",
"description": "Upgrade to the Starter plan and get $20 of credit on your first invoice to try the full Koyeb experience",
"cta": "Upgrade"
}
},
"estimatedCost": {
"currentPlan": "{plan} plan",
"free": "Free",
Expand Down
4 changes: 2 additions & 2 deletions src/layouts/main/estimated-costs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function EstimatedCosts() {
}

return (
<div className="mx-4 divide-y rounded-md border bg-neutral">
<>
<div className="row justify-between p-2 font-medium">
<div className="capitalize">
<T id="currentPlan" values={{ plan: organization.plan }} />
Expand All @@ -37,7 +37,7 @@ export function EstimatedCosts() {
{nextInvoiceQuery.isSuccess && <CostsDetails costs={getCosts(nextInvoiceQuery.data)} />}
</>
)}
</div>
</>
);
}

Expand Down
20 changes: 14 additions & 6 deletions src/layouts/main/main-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { GlobalAlert } from './global-alert';
import { HelpLinks } from './help-links';
import { Layout } from './layout';
import { Navigation } from './navigation';
import { OrganizationPlan } from './organization-plan';
import { OrganizationSwitcher } from './organization-switcher';
import { PlatformStatus } from './platform-status';
import { UserMenu } from './user-menu';
Expand Down Expand Up @@ -103,13 +104,20 @@ function Menu({ collapsed = false }: { collapsed?: boolean }) {

<Navigation collapsed={collapsed} />

{!collapsed && <EstimatedCosts />}

<UserMenu collapsed={collapsed} />
<div className="col gap-4">
{!collapsed && (
<div className="mx-4 divide-y rounded-md border bg-neutral">
<UserMenu collapsed={collapsed} />
<FeatureFlag feature="trial" fallback={<EstimatedCosts />}>
<OrganizationPlan />
</FeatureFlag>
</div>
)}

<div className="col gap-2">
<HelpLinks collapsed={collapsed} />
<PlatformStatus collapsed={collapsed} />
<div className="col gap-2">
<HelpLinks collapsed={collapsed} />
<PlatformStatus collapsed={collapsed} />
</div>
</div>
</div>
);
Expand Down
110 changes: 110 additions & 0 deletions src/layouts/main/organization-plan.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import clsx from 'clsx';
import { useState } from 'react';

import { Badge, Floating } from '@koyeb/design-system';
import { useOrganizationUnsafe } from 'src/api/hooks/session';
import { routes } from 'src/application/routes';
import { LinkButton } from 'src/components/link';
import { PlanIcon } from 'src/components/plan-icon';
import { useFeatureFlag } from 'src/hooks/feature-flag';
import { createTranslate, TranslateEnum } from 'src/intl/translate';
import { TrialSummaryPopup } from 'src/modules/trial/trial-summary-popup';

import { EstimatedCosts } from './estimated-costs';

const T = createTranslate('layouts.main.organizationPlan');

export function OrganizationPlan() {
const [open, setOpen] = useState(false);
const organization = useOrganizationUnsafe();
const trial = useFeatureFlag('trial') && organization?.trial !== undefined;

if (organization?.plan === 'hobby') {
return <HobbyPlan />;
}

return (
<Floating
open={open}
setOpen={setOpen}
hover
strategy="fixed"
placement="right-end"
offset={8}
renderReference={(ref, props) => (
<div
ref={ref}
className={clsx('col gap-4 px-3 py-2 text-start transition-colors', open && 'bg-muted/50')}
{...props}
>
<div className="row items-center gap-2">
<div>
<PlanIcon plan={organization?.plan} className="size-6 text-dim" />
</div>

<div>
<T
id="currentPlan"
values={{ plan: organization && <TranslateEnum enum="plans" value={organization.plan} /> }}
/>
</div>

{trial && (
<Badge size={1} color="green" className="ms-auto">
Trial
</Badge>
)}
</div>

{organization?.trial && (
<LinkButton
variant="outline"
size={1}
href={routes.organizationSettings.plans()}
className="w-full"
>
<T id="upgrade" />
</LinkButton>
)}
</div>
)}
renderFloating={(ref, props) =>
trial ? (
<TrialSummaryPopup ref={ref} onClose={() => setOpen(false)} className="z-30" {...props} />
) : (
<div ref={ref} {...props} className="z-30 w-56 rounded-md border bg-popover">
<EstimatedCosts />
</div>
)
}
/>
);
}

function HobbyPlan() {
return (
<div className="col gap-3 px-3 py-2">
<div className="row items-center justify-between gap-2">
<T id="currentPlan" values={{ plan: <TranslateEnum enum="plans" value="hobby" /> }} />

<Badge size={1} color="green">
<T id="free" />
</Badge>
</div>

<div className="col gap-1">
<div>
<T id="upsell.title" />
</div>

<div className="text-xs text-dim">
<T id="upsell.description" />
</div>
</div>

<LinkButton color="gray" size={1} href={routes.organizationSettings.plans()} className="w-full">
<T id="upsell.cta" />
</LinkButton>
</div>
);
}
5 changes: 5 additions & 0 deletions src/layouts/main/user-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ export function UserMenu({ collapsed }: { collapsed: boolean }) {
>
<UserAvatar user={user} />
{!collapsed && <span className="flex-1 truncate font-medium">{user?.name}</span>}
{!collapsed && (
<span>
<IconChevronRight className="size-4 text-dim" />
</span>
)}
</button>
)}
renderFloating={(ref, props) => (
Expand Down
60 changes: 60 additions & 0 deletions src/modules/trial/trial-summary-popup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import clsx from 'clsx';
import { forwardRef } from 'react';

import { Badge, ProgressBar } from '@koyeb/design-system';
import { useOrganization } from 'src/api/hooks/session';
import { routes } from 'src/application/routes';
import { LinkButton } from 'src/components/link';
import { createTranslate, TranslateEnum } from 'src/intl/translate';

const T = createTranslate('modules.trial.summaryPopup');

type TrialSummaryPopupProps = React.ComponentProps<'div'> & {
onClose: () => void;
};

export const TrialSummaryPopup = forwardRef<HTMLDivElement, TrialSummaryPopupProps>(
function TrialSummaryPopup({ onClose, className, ...props }, ref) {
const organization = useOrganization();

return (
<div ref={ref} {...props} className={clsx('w-56 rounded-md border bg-popover', className)}>
<div className="row justify-between border-b p-3">
<T id="currentPlan" values={{ plan: <TranslateEnum enum="plans" value={organization.plan} /> }} />

<Badge size={1} color="green" className="ms-auto">
<T id="badge" />
</Badge>
</div>

<div className="col gap-3 p-3">
<div className="row justify-between text-dim">
<div>
<T id="usage" />
</div>
<div>$0.00</div>
</div>

<hr />

<div className="row justify-between">
<div className="font-medium">
<T id="creditLeft" />
</div>
<div className="text-green">$10.00</div>
</div>

<ProgressBar progress={1} label={false} />

<div className="text-center text-xs text-dim">
<T id="timeLeft" values={{ days: 7 }} />
</div>

<LinkButton color="gray" href={routes.organizationSettings.billing()} onClick={onClose}>
<T id="cta" />
</LinkButton>
</div>
</div>
);
},
);

0 comments on commit 8f65a94

Please sign in to comment.