Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashboard): Billing settings page in dashboard v2 #7203

Merged
merged 50 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
4a4bf00
feat: settings page
scopsy Dec 3, 2024
062b172
feat: base billing page
scopsy Dec 3, 2024
9a5a375
feat: add billingbase
scopsy Dec 3, 2024
5e76db8
feat: padi
scopsy Dec 3, 2024
bb558f5
feat: add hubspot forms
scopsy Dec 3, 2024
f1a8e6d
feat: add tests
scopsy Dec 3, 2024
33a606f
feat:ff
scopsy Dec 3, 2024
0d49ca8
feat:cspell
scopsy Dec 3, 2024
4c6a1dc
Update .source
scopsy Dec 3, 2024
7cbb10b
fix: v2 dashboard path
scopsy Dec 3, 2024
e462996
feat: align styles
scopsy Dec 3, 2024
0ab6a0a
Update settings.tsx
scopsy Dec 3, 2024
7daa076
fix: style
scopsy Dec 3, 2024
aef9c43
Merge branch 'next' into feat-settings-page
scopsy Dec 3, 2024
b9f2853
Merge branch 'feat-settings-page' into billing-age
scopsy Dec 3, 2024
f1eb4fb
fix: width
scopsy Dec 3, 2024
377a9ab
fix: rows
scopsy Dec 3, 2024
abcce8b
Update settings.tsx
scopsy Dec 3, 2024
465fa4e
fix: team invite CTA
scopsy Dec 3, 2024
0a8d6d6
fix: space
scopsy Dec 3, 2024
8f9f452
fix: lint
scopsy Dec 3, 2024
628a784
fix: compare plans
scopsy Dec 3, 2024
1562165
fix: broken import
scopsy Dec 3, 2024
16ee5a5
Merge branch 'feat-settings-page' into billing-age
scopsy Dec 3, 2024
bf051dc
Merge branch 'next' into feat-settings-page
scopsy Dec 3, 2024
93d5957
fix: update src
scopsy Dec 3, 2024
0333136
Merge branch 'feat-settings-page' into billing-age
scopsy Dec 3, 2024
1036aa9
Merge branch 'next' into billing-age
scopsy Dec 5, 2024
7b09e3e
feat: pointer latest
scopsy Dec 5, 2024
d710cff
Merge branch 'next' into billing-age
scopsy Dec 5, 2024
189a815
Update .source
scopsy Dec 5, 2024
64d6360
fix: boolean pipe
scopsy Dec 5, 2024
70afd1c
fix: small refactor
scopsy Dec 5, 2024
069ca18
Update settings.tsx
scopsy Dec 5, 2024
27372c8
feat: pr review
scopsy Dec 5, 2024
0f6583f
fix: context
scopsy Dec 5, 2024
4e8df6e
fix: states
scopsy Dec 5, 2024
69b4d44
Update get-portal-link.e2e-ee.ts
scopsy Dec 5, 2024
9d808b5
fix: add telemtry
scopsy Dec 5, 2024
02428a2
fix: trial
scopsy Dec 5, 2024
ca47b6a
Merge branch 'next' into billing-age
scopsy Dec 5, 2024
3a8a1c6
feat: add dashboard labeler
scopsy Dec 5, 2024
d5783a2
Merge branch 'next' into billing-age
scopsy Dec 8, 2024
78eb4d0
fix: refactoring
scopsy Dec 8, 2024
c837b97
fix: navigation link and pr comments
scopsy Dec 8, 2024
4d76955
Merge branch 'next' into billing-age
scopsy Dec 8, 2024
372dd89
fix: lint errors
scopsy Dec 8, 2024
a19e0cb
Merge branch 'next' into billing-age
scopsy Dec 8, 2024
bdf8092
fix: pointer
scopsy Dec 9, 2024
fc19f6f
Merge branch 'next' into billing-age
scopsy Dec 9, 2024
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
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,7 @@
"truncatewords",
"xmlschema",
"jsonify",
"hsforms",
"touchpoint",
"Angularjs",
"navigatable"
Expand Down
2 changes: 2 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
- apps/worker/**/*
'@novu/web':
- apps/web/**/*
'@novu/dashboard':
- apps/dashboard/**/*
'@novu/ws':
- apps/ws/**/*
'@novu/inbound-mail':
Expand Down
2 changes: 1 addition & 1 deletion .source
2 changes: 1 addition & 1 deletion apps/api/src/app/testing/billing/get-portal-link.e2e-ee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe('Get portal link', async () => {
});

expect(stub.lastCall.args.at(0)).to.deep.equal({
return_url: `${process.env.FRONT_BASE_URL}/settings/billing`,
return_url: `${process.env.FRONT_BASE_URL}/manage-account/billing`,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path was previously wrong

customer: 'customer_id',
});

Expand Down
131 changes: 131 additions & 0 deletions apps/dashboard/src/components/billing/active-plan-banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { Badge } from '@/components/primitives/badge';
import { Card } from '@/components/primitives/card';
import { Progress } from '@/components/primitives/progress';
import { cn } from '../../utils/ui';
import { CalendarDays } from 'lucide-react';
import { PlanActionButton } from './plan-action-button';
import { Skeleton } from '@/components/primitives/skeleton';
import { useFetchSubscription } from '../../hooks/use-fetch-subscription';

interface ActivePlanBannerProps {
selectedBillingInterval: 'month' | 'year';
}

export function ActivePlanBanner({ selectedBillingInterval }: ActivePlanBannerProps) {
const { subscription, daysLeft } = useFetchSubscription();

const getProgressColor = (current: number, max: number) => {
const percentage = (current / max) * 100;
if (percentage > 90) return 'text-destructive';
if (percentage > 75) return 'text-warning';

return '';
};

const getProgressBarColor = (current: number, max: number) => {
const percentage = (current / max) * 100;
if (percentage > 90) return 'bg-gradient-to-r from-red-500 to-red-400';
if (percentage > 75) return 'bg-gradient-to-r from-amber-500 to-amber-400';

return 'bg-gradient-to-r from-emerald-500 to-emerald-400';
};

const formatDate = (date: string | number) => {
return new Date(date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
};

return (
<div className="mt-6 flex space-y-3">
<Card className="mx-auto w-full max-w-[500px] overflow-hidden border shadow-none">
<div className="space-y-5 p-5">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<div className="flex items-center gap-3">
{!subscription ? (
<Skeleton className="h-7 w-24" />
) : (
<h3 className="text-lg font-semibold capitalize">{subscription.apiServiceLevel?.toLowerCase()}</h3>
)}
{subscription?.trial.isActive && (
<Badge variant="outline" className="font-medium">
Trial
</Badge>
)}
</div>
{subscription?.trial.isActive && (
<div className="text-warning text-sm font-medium">{daysLeft} days left for trial</div>
)}
</div>

<PlanActionButton
selectedBillingInterval={selectedBillingInterval}
variant="outline"
size="sm"
className="shrink-0"
/>
</div>

<div className="space-y-4">
<div className="text-muted-foreground flex items-center gap-2 text-sm">
<CalendarDays className="h-4 w-4" />
{!subscription ? (
<Skeleton className="h-4 w-48" />
) : (
<span>
{formatDate(subscription.currentPeriodStart ?? Date.now())} -{' '}
{formatDate(subscription.currentPeriodEnd ?? Date.now())}
</span>
)}
</div>

<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
{subscription ? (
<>
<div>
<span
className={cn(
'font-medium',
getProgressColor(subscription.events.current ?? 0, subscription.events.included ?? 0)
)}
>
{subscription.events.current?.toLocaleString() ?? 0}
</span>{' '}
<span className="text-muted-foreground">
of {subscription.events.included?.toLocaleString() ?? 0} events
</span>
</div>
<span className="text-muted-foreground text-xs">Updates hourly</span>
</>
) : (
<>
<Skeleton className="h-4 w-36" />
<Skeleton className="h-4 w-24" />
</>
)}
</div>
{subscription ? (
<Progress
value={Math.min(
((subscription.events.current ?? 0) / (subscription.events.included ?? 0)) * 100,
100
)}
className={cn(
'h-1.5 rounded-lg transition-all duration-300',
getProgressBarColor(subscription.events.current ?? 0, subscription.events.included ?? 0)
)}
/>
) : (
<Skeleton className="h-1.5 w-full" />
)}
</div>
</div>
</div>
</Card>
</div>
);
}
44 changes: 44 additions & 0 deletions apps/dashboard/src/components/billing/contact-sales-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Button } from '@/components/primitives/button';
import { ApiServiceLevelEnum } from '@novu/shared';
import { useState } from 'react';
import { ContactSalesModal } from './contact-sales-modal';
import { useTelemetry } from '../../hooks/use-telemetry';
import { TelemetryEvent } from '../../utils/telemetry';

interface ContactSalesButtonProps {
className?: string;
variant?: 'default' | 'outline';
}

export function ContactSalesButton({ className, variant = 'outline' }: ContactSalesButtonProps) {
const [isContactSalesModalOpen, setIsContactSalesModalOpen] = useState(false);
const track = useTelemetry();

const handleContactSales = () => {
track(TelemetryEvent.BILLING_CONTACT_SALES_CLICKED, {
intendedPlan: ApiServiceLevelEnum.ENTERPRISE,
source: 'billing_page',
});
setIsContactSalesModalOpen(true);
};

const handleModalClose = () => {
track(TelemetryEvent.BILLING_CONTACT_SALES_MODAL_CLOSED, {
intendedPlan: ApiServiceLevelEnum.ENTERPRISE,
});
setIsContactSalesModalOpen(false);
};

return (
<>
<Button variant={variant} className={className} onClick={handleContactSales}>
Contact sales
</Button>
<ContactSalesModal
isOpen={isContactSalesModalOpen}
onClose={handleModalClose}
intendedApiServiceLevel={ApiServiceLevelEnum.ENTERPRISE}
/>
</>
);
}
47 changes: 47 additions & 0 deletions apps/dashboard/src/components/billing/contact-sales-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/primitives/dialog';
import { ApiServiceLevelEnum } from '@novu/shared';
import { HubspotForm } from '../hubspot-form';
import { HUBSPOT_FORM_IDS } from './utils/hubspot.constants';
import { useAuth } from '@/context/auth/hooks';
import { toast } from 'sonner';

interface ContactSalesModalProps {
isOpen: boolean;
onClose: () => void;
intendedApiServiceLevel: ApiServiceLevelEnum;
}

export function ContactSalesModal({ isOpen, onClose, intendedApiServiceLevel }: ContactSalesModalProps) {
const { currentUser, currentOrganization } = useAuth();

if (!isOpen || !currentUser || !currentOrganization) {
return null;
}

return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="p-10 sm:max-w-[840px]">
<DialogHeader>
<DialogTitle>Contact sales</DialogTitle>
</DialogHeader>
<HubspotForm
formId={HUBSPOT_FORM_IDS.UPGRADE_CONTACT_SALES}
properties={{
firstname: currentUser.firstName || '',
lastname: currentUser.lastName || '',
email: currentUser.email || '',
app_organizationid: currentOrganization._id,
'TICKET.subject': `Contact Sales - ${intendedApiServiceLevel}`,
'TICKET.content': '',
}}
readonlyProperties={['email']}
focussedProperty="TICKET.content"
onFormSubmitted={() => {
toast.success('Thank you for contacting us! We will be in touch soon.');
onClose();
}}
/>
</DialogContent>
</Dialog>
);
}
Loading
Loading