diff --git a/.source b/.source index c9d04dd7b0f..f6e9050d67e 160000 --- a/.source +++ b/.source @@ -1 +1 @@ -Subproject commit c9d04dd7b0fcaa88f87e5dd5a0ef1e58e08b1fa3 +Subproject commit f6e9050d67eefd51df752e947febd2dba3e76be6 diff --git a/apps/dashboard/src/components/primitives/badge.tsx b/apps/dashboard/src/components/primitives/badge.tsx index e6bbda2eab7..48310161fb7 100644 --- a/apps/dashboard/src/components/primitives/badge.tsx +++ b/apps/dashboard/src/components/primitives/badge.tsx @@ -7,7 +7,7 @@ const badgeVariants = cva( { variants: { variant: { - default: 'border-transparent bg-primary/5 text-primary/50', + default: 'border-transparent bg-secondary-alpha-100 text-secondary-500', destructive: 'border-transparent bg-destructive/10 text-destructive', success: 'border-transparent bg-success/10 text-success', warning: 'border-transparent bg-warning/10 text-warning', diff --git a/apps/dashboard/src/components/primitives/button.tsx b/apps/dashboard/src/components/primitives/button.tsx index da2648cfe5a..fa8916619ee 100644 --- a/apps/dashboard/src/components/primitives/button.tsx +++ b/apps/dashboard/src/components/primitives/button.tsx @@ -9,13 +9,13 @@ const buttonVariants = cva( variants: { variant: { default: + 'bg-gradient-to-b from-secondary-alpha-900 to-secondary-900 text-secondary-foreground shadow-[inset_0_-4px_2px_-2px_hsl(var(--secondary-900)),inset_0_0_0_1px_rgba(255,255,255,0.16),0_0_0_1px_hsl(var(--secondary-900)),0px_1px_2px_0px_#0E121B3D] after:content-[""] after:absolute after:w-full after:h-full after:bg-gradient-to-b after:from-background/10 after:opacity-0 hover:after:opacity-100 after:rounded-lg after:transition-opacity after:duration-300', + primary: 'bg-gradient-to-b from-primary/90 to-primary text-primary-foreground shadow-[inset_0_-4px_2px_-2px_hsl(var(--primary)),inset_0_0_0_1px_rgba(255,255,255,0.16),0_0_0_1px_hsl(var(--primary)),0px_1px_2px_0px_#0E121B3D] after:content-[""] after:absolute after:w-full after:h-full after:bg-gradient-to-b after:from-background/10 after:opacity-0 hover:after:opacity-100 after:rounded-lg after:transition-opacity after:duration-300', - novu: 'bg-gradient-to-b from-novu/90 to-novu text-novu-foreground shadow-[inset_0_-4px_2px_-2px_hsl(var(--novu)),inset_0_0_0_1px_rgba(255,255,255,0.16),0_0_0_1px_hsl(var(--novu)),0px_1px_2px_0px_#0E121B3D] after:content-[""] after:absolute after:w-full after:h-full after:bg-gradient-to-b after:from-background/10 after:opacity-0 hover:after:opacity-100 after:rounded-lg after:transition-opacity after:duration-300', destructive: 'bg-gradient-to-b from-destructive/90 to-destructive text-destructive-foreground shadow-[inset_0_-4px_2px_-2px_hsl(var(--destructive)),inset_0_0_0_1px_rgba(255,255,255,0.16),0_0_0_1px_hsl(var(--destructive)),0px_1px_2px_0px_#0E121B3D] after:content-[""] after:absolute after:w-full after:h-full after:bg-gradient-to-b after:from-background/10 after:opacity-0 hover:after:opacity-100 after:rounded-lg after:transition-opacity after:duration-300', outline: 'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground', - ghost: 'hover:bg-accent hover:text-accent-foreground', - link: 'text-primary underline-offset-4 hover:underline', + ghost: 'hover:bg-accent', }, size: { default: 'h-9 p-2.5', diff --git a/apps/dashboard/src/components/primitives/popover.tsx b/apps/dashboard/src/components/primitives/popover.tsx index 34e82d23bfe..69d102de81d 100644 --- a/apps/dashboard/src/components/primitives/popover.tsx +++ b/apps/dashboard/src/components/primitives/popover.tsx @@ -19,6 +19,7 @@ const PopoverContent = React.forwardRef< align={align} sideOffset={sideOffset} className={cn( + //TODO: Change bg and text colors 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md outline-none', className )} diff --git a/apps/dashboard/src/components/primitives/step.tsx b/apps/dashboard/src/components/primitives/step.tsx index 826e158df6a..b6cd556add1 100644 --- a/apps/dashboard/src/components/primitives/step.tsx +++ b/apps/dashboard/src/components/primitives/step.tsx @@ -8,7 +8,7 @@ const stepVariants = cva( { variants: { variant: { - default: 'border-primary/5 bg-primary/2.5 text-primary/30', + default: 'border-secondary-alpha-100 bg-secondary-alpha-50 text-secondary-alpha-400', feature: 'border-feature/30 bg-foreground/2.5 text-feature/30', information: 'border-information/30 bg-foreground/2.5 text-information/30', highlighted: 'border-highlighted/30 bg-foreground/2.5 text-highlighted/30', diff --git a/apps/dashboard/src/components/primitives/tag.tsx b/apps/dashboard/src/components/primitives/tag.tsx index e5194e70cb2..fbfdfd8eec9 100644 --- a/apps/dashboard/src/components/primitives/tag.tsx +++ b/apps/dashboard/src/components/primitives/tag.tsx @@ -8,7 +8,7 @@ const tagVariants = cva( { variants: { variant: { - default: 'border-transparent bg-primary/5 text-primary/50', + default: 'border-transparent bg-secondary-alpha-100 text-secondary-alpha-500', feature: 'border-transparent bg-feature/10 text-feature', information: 'border-transparent bg-information/10 text-information', }, diff --git a/apps/dashboard/src/index.css b/apps/dashboard/src/index.css index c1527db1d17..56f693ccdfd 100644 --- a/apps/dashboard/src/index.css +++ b/apps/dashboard/src/index.css @@ -4,22 +4,52 @@ @layer base { :root { --background: 0 0% 100%; - --foreground: 222 32% 8%; - --card: 0 0% 98%; - --card-foreground: 222 32% 8%; + --foreground-0: 210 10% 100%; + --foreground-50: 210 10% 96%; + --foreground-100: 210 30% 96%; + --foreground-200: 220 18% 90%; + --foreground-300: 219 15% 82%; + --foreground-400: 220 11% 64%; + --foreground-500: 221 8% 48%; + --foreground-600: 222 11% 36%; + --foreground-700: 221 16% 20%; + --foreground-800: 227 17% 16%; + --foreground-900: 226 21% 12%; + --foreground-950: 222 32% 8%; - --popover: 0 0% 100%; - --popover-foreground: 222 32% 8%; + --primary: 346 73% 50%; + --primary-foreground: 0 0% 100%; - --novu: 346 73% 50%; - --novu-foreground: 0 0% 100%; + --secondary-0: 210 10% 100%; + --secondary-50: 210 10% 96%; + --secondary-100: 210 30% 96%; + --secondary-200: 220 18% 90%; + --secondary-300: 219 15% 82%; + --secondary-400: 220 11% 64%; + --secondary-500: 221 8% 48%; + --secondary-600: 222 11% 36%; + --secondary-700: 221 16% 20%; + --secondary-800: 227 17% 16%; + --secondary-900: 226 21% 12%; + --secondary-950: 222 32% 8%; + --secondary: 0 0 0%; + --secondary-foreground: 0 0% 100%; - --primary: 222 32% 8%; - --primary-foreground: 0 0% 100%; + /* Secondary scale in alpha that looks the same on white background */ + --secondary-alpha-50: 0 0% 69% / 0.05; + --secondary-alpha-100: 210 30% 61% / 0.1; + --secondary-alpha-200: 220 18% 50% / 0.2; + --secondary-alpha-300: 218 23% 40% / 0.3; + --secondary-alpha-400: 220 100% 10% / 0.4; + --secondary-alpha-500: 240 100% 2% / 0.5; + --secondary-alpha-600: 0 0% 0% / 0.6; + --secondary-alpha-700: 0 0% 0% / 0.7; + --secondary-alpha-800: 0 0% 0% / 0.8; + --secondary-alpha-900: 231 100% 3% / 0.9; + --secondary-alpha-950: 219 88% 3% / 0.95; --accent: 0 0% 96.1%; - --accent-foreground: 222 32% 8%; --destructive: 355 96% 60%; --destructive-foreground: 0 0% 100%; @@ -40,12 +70,6 @@ --input: 0 0% 89.8%; --ring: 222 32% 8%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --radius: 0.5rem; } } @@ -54,6 +78,6 @@ @apply border-border; } body { - @apply bg-background text-foreground; + @apply bg-background text-foreground-950; } } diff --git a/apps/dashboard/src/routes/primitives.tsx b/apps/dashboard/src/routes/primitives.tsx index 043eff6dd8a..166c26e73b2 100644 --- a/apps/dashboard/src/routes/primitives.tsx +++ b/apps/dashboard/src/routes/primitives.tsx @@ -22,8 +22,8 @@ export const Primitives = () => {

Home page

Buttons

- - + + @@ -31,7 +31,7 @@ export const Primitives = () => {

Badges

- Primary badge + Secondary badge Success badge Destructive badge Warning badge diff --git a/apps/dashboard/tailwind.config.js b/apps/dashboard/tailwind.config.js index 6ba90fc0420..7afb3664a2f 100644 --- a/apps/dashboard/tailwind.config.js +++ b/apps/dashboard/tailwind.config.js @@ -8,6 +8,86 @@ export default { sm: '0px 1px 2px 0px #1018280F,0px 1px 3px 0px #1018281A', DEFAULT: '0px 16px 32px -12px #0E121B1A', }, + colors: { + background: 'hsl(var(--background))', + foreground: { + 0: 'hsl(var(--foreground-0))', + 50: 'hsl(var(--foreground-50))', + 100: 'hsl(var(--foreground-100))', + 200: 'hsl(var(--foreground-200))', + 300: 'hsl(var(--foreground-300))', + 400: 'hsl(var(--foreground-400))', + 500: 'hsl(var(--foreground-500))', + 600: 'hsl(var(--foreground-600))', + 700: 'hsl(var(--foreground-700))', + 800: 'hsl(var(--foreground-800))', + 900: 'hsl(var(--foreground-900))', + 950: 'hsl(var(--foreground-950))', + }, + neutral: { + DEFAULT: 'hsl(var(--secondary))', + 0: 'hsl(var(--secondary-0))', + 50: 'hsl(var(--secondary-50))', + 100: 'hsl(var(--secondary-100))', + 200: 'hsl(var(--secondary-200))', + 300: 'hsl(var(--secondary-300))', + 400: 'hsl(var(--secondary-400))', + 500: 'hsl(var(--secondary-500))', + 600: 'hsl(var(--secondary-600))', + 700: 'hsl(var(--secondary-700))', + 800: 'hsl(var(--secondary-800))', + 900: 'hsl(var(--secondary-900))', + 950: 'hsl(var(--secondary-950))', + 1000: 'hsl(var(--secondary-1000))', + foreground: 'hsl(var(--secondary-foreground))', + }, + 'secondary-alpha': { + 50: 'hsl(var(--secondary-alpha-50))', + 100: 'hsl(var(--secondary-alpha-100))', + 200: 'hsl(var(--secondary-alpha-200))', + 300: 'hsl(var(--secondary-alpha-300))', + 400: 'hsl(var(--secondary-alpha-400))', + 500: 'hsl(var(--secondary-alpha-500))', + 600: 'hsl(var(--secondary-alpha-600))', + 700: 'hsl(var(--secondary-alpha-700))', + 800: 'hsl(var(--secondary-alpha-800))', + 900: 'hsl(var(--secondary-alpha-900))', + 950: 'hsl(var(--secondary-alpha-950))', + 1000: 'hsl(var(--secondary-alpha-1000))', + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))', + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))', + }, + success: { + DEFAULT: 'hsl(var(--success))', + }, + warning: { + DEFAULT: 'hsl(var(--warning))', + }, + feature: { + DEFAULT: 'hsl(var(--feature))', + }, + information: { + DEFAULT: 'hsl(var(--information))', + }, + highlighted: { + DEFAULT: 'hsl(var(--highlighted))', + }, + stable: { + DEFAULT: 'hsl(var(--stable))', + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + }, extend: { opacity: { 2.5: 0.025, @@ -17,62 +97,6 @@ export default { md: 'calc(var(--radius) - 2px)', sm: 'calc(var(--radius) - 4px)', }, - colors: { - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - card: { - DEFAULT: 'hsl(var(--card))', - foreground: 'hsl(var(--card-foreground))', - }, - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))', - }, - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))', - }, - novu: { - DEFAULT: 'hsl(var(--novu))', - foreground: 'hsl(var(--novu-foreground))', - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))', - }, - success: { - DEFAULT: 'hsl(var(--success))', - }, - warning: { - DEFAULT: 'hsl(var(--warning))', - }, - feature: { - DEFAULT: 'hsl(var(--feature))', - }, - information: { - DEFAULT: 'hsl(var(--information))', - }, - highlighted: { - DEFAULT: 'hsl(var(--highlighted))', - }, - stable: { - DEFAULT: 'hsl(var(--stable))', - }, - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - chart: { - 1: 'hsl(var(--chart-1))', - 2: 'hsl(var(--chart-2))', - 3: 'hsl(var(--chart-3))', - 4: 'hsl(var(--chart-4))', - 5: 'hsl(var(--chart-5))', - }, - }, }, }, plugins: [require('tailwindcss-animate')], diff --git a/apps/web/src/ee/billing/components/Plan.tsx b/apps/web/src/ee/billing/components/Plan.tsx index ce870fcad0e..a6beb907a8f 100644 --- a/apps/web/src/ee/billing/components/Plan.tsx +++ b/apps/web/src/ee/billing/components/Plan.tsx @@ -8,15 +8,11 @@ import { planList } from '../utils/planList'; import { PlanFooter } from './PlanFooter'; import { FreeTrialPlanWidget } from './FreeTrialPlanWidget'; import { useSubscriptionContext } from './SubscriptionProvider'; -import { ActivePlanBanner } from './billingV2/ActivePlanBanner'; -import { useFeatureFlag } from '../../../hooks'; -import { FeatureFlagsKeysEnum } from '@novu/shared'; export const Plan = () => { const { colorScheme } = useMantineTheme(); const isDark = colorScheme === 'dark'; const { isLoading, trial } = useSubscriptionContext(); - const isImprovedBillingEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_IMPROVED_BILLING_ENABLED); if (isLoading || trial.daysLeft === null) { return ( @@ -28,8 +24,7 @@ export const Plan = () => { return ( <> - {!isImprovedBillingEnabled && } - {isImprovedBillingEnabled && } + {planList.map((row, index) => ( diff --git a/apps/web/src/ee/billing/components/PlanHeader.tsx b/apps/web/src/ee/billing/components/PlanHeader.tsx index 2563c3e0f37..5ea575ea2e5 100644 --- a/apps/web/src/ee/billing/components/PlanHeader.tsx +++ b/apps/web/src/ee/billing/components/PlanHeader.tsx @@ -11,7 +11,6 @@ import { includedEventQuotaFromApiServiceLevel } from '../utils/plan.constants'; import { ContactSalesModal } from './ContactSalesModal'; import { BillingIntervalControl } from './BillingIntervalControl'; import { useSubscriptionContext } from './SubscriptionProvider'; -import { useFeatureFlag } from '../../../hooks'; const black = colors.BGDark; @@ -40,18 +39,8 @@ export const PlanHeader = () => { isLoadingSubscriptionData ? subscriptionApiServiceLevel : ApiServiceLevelEnum.FREE ); const [billingInterval, setBillingInterval] = useState<'month' | 'year'>(subscriptionBillingInterval || 'month'); - const isImprovedBillingEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_IMPROVED_BILLING_ENABLED); - const isStripeCheckoutEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_STRIPE_CHECKOUT_ENABLED); - - const checkoutUrl = isStripeCheckoutEnabled ? '/v1/billing/checkout-session' : '/v1/billing/checkout'; const checkoutOnSuccess = (data) => { - if (isStripeCheckoutEnabled) { - window.location.href = data.stripeCheckoutUrl; - - return; - } - if (upgradeOpen) { return; } @@ -72,7 +61,7 @@ export const PlanHeader = () => { any, any, { billingInterval: 'month' | 'year'; apiServiceLevel: ApiServiceLevelEnum } - >((data) => api.post(checkoutUrl, data), { + >((data) => api.post('/v1/billing/checkout', data), { onSuccess: checkoutOnSuccess, onError: (e: any) => { errorMessage(e.message || 'Unexpected error'); @@ -80,30 +69,13 @@ export const PlanHeader = () => { }); useEffect(() => { - if (!isStripeCheckoutEnabled) { - if (intentSecret === '') { - return; - } - checkout({ billingInterval, apiServiceLevel: ApiServiceLevelEnum.BUSINESS }); + if (intentSecret === '') { + return; } + checkout({ billingInterval, apiServiceLevel: ApiServiceLevelEnum.BUSINESS }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [billingInterval, intentSecret, apiServiceLevel]); - useEffect(() => { - if (isStripeCheckoutEnabled) { - const checkoutResult = new URLSearchParams(window.location.search).get('result'); - - if (checkoutResult === 'success') { - setApiServiceLevel(ApiServiceLevelEnum.BUSINESS); - successMessage('Payment was successful.'); - } - - if (checkoutResult === 'canceled') { - errorMessage('Order canceled.'); - } - } - }, []); - const { mutateAsync: goToPortal, isLoading: isGoingToPortal } = useMutation( () => api.get('/v1/billing/portal'), { @@ -215,12 +187,9 @@ export const PlanHeader = () => { }); }} > - - Upgrade - Add payment method - Update payment method - - Upgrade + Upgrade + Add payment method + Update payment method @@ -277,27 +246,25 @@ export const PlanHeader = () => {
- {!isStripeCheckoutEnabled ? ( - { - setUpgradeOpen(false); - }} - onSucceeded={() => { - setApiServiceLevel(ApiServiceLevelEnum.BUSINESS); - setUpgradeOpen(false); - }} - onContactSales={() => { - setUpgradeOpen(false); - setIntendedApiServiceLevel(ApiServiceLevelEnum.BUSINESS); - setIsContactSalesModalOpen(true); - }} - /> - ) : null} + { + setUpgradeOpen(false); + }} + onSucceeded={() => { + setApiServiceLevel(ApiServiceLevelEnum.BUSINESS); + setUpgradeOpen(false); + }} + onContactSales={() => { + setUpgradeOpen(false); + setIntendedApiServiceLevel(ApiServiceLevelEnum.BUSINESS); + setIsContactSalesModalOpen(true); + }} + /> { diff --git a/apps/web/src/ee/billing/components/UpgradeSubmitButton.tsx b/apps/web/src/ee/billing/components/UpgradeSubmitButton.tsx index ecd4d3e6ba5..ee9f247b4c1 100644 --- a/apps/web/src/ee/billing/components/UpgradeSubmitButton.tsx +++ b/apps/web/src/ee/billing/components/UpgradeSubmitButton.tsx @@ -3,8 +3,6 @@ import { useStripe, useElements } from '@stripe/react-stripe-js'; import { errorMessage, Button, When } from '@novu/design-system'; import { useSegment } from '../../../components/providers/SegmentProvider'; import { useSubscription } from '../hooks/useSubscription'; -import { useFeatureFlag } from '../../../hooks'; -import { FeatureFlagsKeysEnum } from '@novu/shared'; export const UpgradeSubmitButton = ({ intervalChanging }: { intervalChanging: boolean }) => { const segment = useSegment(); @@ -12,7 +10,6 @@ export const UpgradeSubmitButton = ({ intervalChanging }: { intervalChanging: bo const elements = useElements(); const [loading, setLoading] = useState(false); const { trial } = useSubscription(); - const isImprovedBillingEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_IMPROVED_BILLING_ENABLED); const handleSubmit = async (event) => { segment.track('Submit Payment Checkout Clicked - Billing'); @@ -39,11 +36,8 @@ export const UpgradeSubmitButton = ({ intervalChanging }: { intervalChanging: bo return ( ); }; diff --git a/apps/web/src/ee/billing/components/billingV2/ActivePlanBanner.tsx b/apps/web/src/ee/billing/components/billingV2/ActivePlanBanner.tsx index 30fe505d5b6..f68fd752b01 100644 --- a/apps/web/src/ee/billing/components/billingV2/ActivePlanBanner.tsx +++ b/apps/web/src/ee/billing/components/billingV2/ActivePlanBanner.tsx @@ -1,15 +1,13 @@ import { Text, Title } from '@novu/novui'; -import { Badge, Button, useMantineTheme } from '@mantine/core'; +import { MantineTheme } from '@mantine/core'; import { css } from '@novu/novui/css'; import { UsageProgress } from './UsageProgress'; -import { errorMessage } from '@novu/design-system'; -import { useMutation } from '@tanstack/react-query'; -import { api } from '../../../../api'; -import { useSegment } from '../../../../components/providers/SegmentProvider'; import { useSubscriptionContext } from '../SubscriptionProvider'; import { capitalizeFirstLetter } from '../../../../utils/string'; +import { Badge } from './Badge'; +import { PlanActionButton } from './PlanActionButton'; -export const ActivePlanBanner = () => { +export const ActivePlanBanner = ({ selectedBillingInterval }: { selectedBillingInterval: 'month' | 'year' }) => { const { apiServiceLevel, status, events, trial } = useSubscriptionContext(); return ( @@ -20,7 +18,7 @@ export const ActivePlanBanner = () => {
- + ); @@ -28,16 +26,13 @@ export const ActivePlanBanner = () => { const PlanHeader = ({ apiServiceLevel, isFreeTrialActive, daysLeft }) => { const color = getColorByDaysLeft(daysLeft); - const theme = useMantineTheme(); return (
{capitalizeFirstLetter(apiServiceLevel)} {isFreeTrialActive && ( <> - - Trial - +
{daysLeft} @@ -60,7 +55,7 @@ const PlanInfo = ({ apiServiceLevel, currentEvents, maxEvents }) => {
- {currentEvents.toLocaleString()} + {currentEvents?.toLocaleString()} events
@@ -72,37 +67,10 @@ const PlanInfo = ({ apiServiceLevel, currentEvents, maxEvents }) => { ); }; -const PlanActions = ({ trialEnd, status }) => { - const segment = useSegment(); - - const handleManageSubscription = () => { - segment.track('Manage Subscription Clicked - Plans List'); - goToPortal({}); - }; - - const { mutateAsync: goToPortal, isLoading: isGoingToPortal } = useMutation( - () => api.get('/v1/billing/portal'), - { - onSuccess: (url) => { - window.location.href = url; - }, - onError: (e: any) => { - errorMessage(e.message || 'Unexpected error'); - }, - } - ); - +const PlanActions = ({ trialEnd, status, selectedBillingInterval }) => { return (
- {status === 'active' ? ( - - ) : null} + {status === 'trialing' ? ( Trial ends on {formatDate(trialEnd)} @@ -116,12 +84,14 @@ const getColorByEventsUsed = (eventsUsed: number, maxEvents: number) => { const percentage = (eventsUsed / maxEvents) * 100; if (percentage >= 100) return '#F2555A'; if (percentage >= 90) return '#FFB224'; + return undefined; }; const getColorByDaysLeft = (daysLeft: number) => { if (daysLeft <= 0) return '#F2555A'; if (daysLeft <= 3) return '#FFB224'; + return undefined; }; @@ -163,7 +133,7 @@ const styles = { alignItems: 'center', gap: '8px', }), - trialBadge: (theme) => + trialBadge: (theme: MantineTheme) => css({ // TODO: replace with 'mauve.80' color token when legacy tokens are removed background: theme.colorScheme === 'dark' ? '#2E2E32 !important' : '#e9e8eaff !important', diff --git a/apps/web/src/ee/billing/components/billingV2/Badge.tsx b/apps/web/src/ee/billing/components/billingV2/Badge.tsx new file mode 100644 index 00000000000..b0744bcc569 --- /dev/null +++ b/apps/web/src/ee/billing/components/billingV2/Badge.tsx @@ -0,0 +1,23 @@ +import { BadgeProps, Badge as MantineBadge, MantineTheme, useMantineTheme } from '@mantine/core'; +import { css } from '@novu/novui/css'; + +export const Badge = ({ label, ...props }: BadgeProps & { label?: React.ReactNode }) => { + const theme = useMantineTheme(); + + return ( + + {label} + + ); +}; + +const styles = { + badge: (theme: MantineTheme) => + css({ + // TODO: replace with 'mauve.80' color token when legacy tokens are removed + background: theme.colorScheme === 'dark' ? '#2E2E32 !important' : '#e4e2e4ff !important', + padding: '2px 8px !important', + color: theme.colorScheme === 'dark' ? '#7E7D86' : '#86848dff', + borderRadius: '24px !important', + }), +}; diff --git a/apps/web/src/ee/billing/components/billingV2/ContactUsButton.tsx b/apps/web/src/ee/billing/components/billingV2/ContactUsButton.tsx new file mode 100644 index 00000000000..a5324e8c083 --- /dev/null +++ b/apps/web/src/ee/billing/components/billingV2/ContactUsButton.tsx @@ -0,0 +1,31 @@ +import { Button } from '@novu/novui'; +import { css } from '@novu/novui/css'; +import { ApiServiceLevelEnum } from '@novu/shared'; +import { useState } from 'react'; +import { ContactSalesModal } from '../ContactSalesModal'; + +export const ContactUsButton = () => { + const [isContactSalesModalOpen, setIsContactSalesModalOpen] = useState(false); + + return ( + <> + + { + setIsContactSalesModalOpen(false); + }} + intendedApiServiceLevel={ApiServiceLevelEnum.ENTERPRISE} + /> + + ); +}; + +const styles = { + contactButton: css({ + background: '#34343A !important', + fontWeight: '400 !important', + }), +}; diff --git a/apps/web/src/ee/billing/components/billingV2/Features.tsx b/apps/web/src/ee/billing/components/billingV2/Features.tsx new file mode 100644 index 00000000000..f7d2571829e --- /dev/null +++ b/apps/web/src/ee/billing/components/billingV2/Features.tsx @@ -0,0 +1,373 @@ +import { css } from '@novu/novui/css'; +import { Text } from '@novu/novui'; +import styled from '@emotion/styled'; +import { ApiServiceLevelEnum } from '@novu/shared'; +import { IconCheck as _IconCheck } from '@novu/novui/icons'; +import { Badge } from './Badge'; + +const TitleCell = styled.div` + display: flex; + padding: 16px 24px; + flex-direction: column; + align-items: flex-start; + align-self: stretch; + flex: 1 0 0; +`; + +const Cell = styled.div` + display: flex; + padding: 16px; + flex: 1 0 0; + align-self: stretch; + flex-direction: column; + gap: 8px; + justify-content: center; + align-items: center; +`; + +const IconCheck = () => ( + <_IconCheck + className={css({ + color: { + base: 'typography.text.primary !important', + _dark: 'typography.text.main !important', + }, + })} + /> +); + +enum SupportedPlansEnum { + FREE = ApiServiceLevelEnum.FREE, + BUSINESS = ApiServiceLevelEnum.BUSINESS, + ENTERPRISE = ApiServiceLevelEnum.ENTERPRISE, +} + +type FeatureValue = { + value: React.ReactNode; +}; + +type Feature = { + label: string; + isTitle?: boolean; + isContrast?: boolean; + values: { + [SupportedPlansEnum.FREE]: FeatureValue; + [SupportedPlansEnum.BUSINESS]: FeatureValue; + [SupportedPlansEnum.ENTERPRISE]: FeatureValue; + }; +}; + +const features: Feature[] = [ + { + label: 'Platform', + isContrast: false, + isTitle: true, + values: { + [SupportedPlansEnum.FREE]: { value: '' }, + [SupportedPlansEnum.BUSINESS]: { value: '' }, + [SupportedPlansEnum.ENTERPRISE]: { value: '' }, + }, + }, + { + label: 'Monthly events', + isContrast: false, + values: { + [SupportedPlansEnum.FREE]: { value: 'Up to 30,000' }, + [SupportedPlansEnum.BUSINESS]: { value: 'Up to 250,000' }, + [SupportedPlansEnum.ENTERPRISE]: { value: '5,000,000' }, + }, + }, + { + label: 'Additional Events', + isContrast: true, + values: { + [SupportedPlansEnum.FREE]: { value: '-' }, + [SupportedPlansEnum.BUSINESS]: { value: '$0.0012 per event' }, + [SupportedPlansEnum.ENTERPRISE]: { value: 'Custom' }, + }, + }, + { + label: 'Email, InApp, SMS, Chat, Push Channels', + isContrast: false, + values: { + [SupportedPlansEnum.FREE]: { value: }, + [SupportedPlansEnum.BUSINESS]: { value: }, + [SupportedPlansEnum.ENTERPRISE]: { value: }, + }, + }, + { + label: 'Notification subscribers', + isContrast: true, + values: { + [SupportedPlansEnum.FREE]: { value: 'Unlimited' }, + [SupportedPlansEnum.BUSINESS]: { value: 'Unlimited' }, + [SupportedPlansEnum.ENTERPRISE]: { value: 'Unlimited' }, + }, + }, + { + label: 'Framework', + isContrast: false, + isTitle: true, + values: { + [SupportedPlansEnum.FREE]: { value: '' }, + [SupportedPlansEnum.BUSINESS]: { value: '' }, + [SupportedPlansEnum.ENTERPRISE]: { value: '' }, + }, + }, + { + label: 'Total workflows', + isContrast: false, + values: { + [SupportedPlansEnum.FREE]: { value: 'Unlimited' }, + [SupportedPlansEnum.BUSINESS]: { value: 'Unlimited' }, + [SupportedPlansEnum.ENTERPRISE]: { value: 'Unlimited' }, + }, + }, + { + label: 'Provider integrations', + isContrast: true, + values: { + [SupportedPlansEnum.FREE]: { value: 'Unlimited' }, + [SupportedPlansEnum.BUSINESS]: { value: 'Unlimited' }, + [SupportedPlansEnum.ENTERPRISE]: { value: 'Unlimited' }, + }, + }, + { + label: 'Activity retention', + isContrast: false, + values: { + [SupportedPlansEnum.FREE]: { value: }, + [SupportedPlansEnum.BUSINESS]: { value: }, + [SupportedPlansEnum.ENTERPRISE]: { value: }, + }, + }, + { + label: 'Digests', + isContrast: true, + values: { + [SupportedPlansEnum.FREE]: { value: }, + [SupportedPlansEnum.BUSINESS]: { value: }, + [SupportedPlansEnum.ENTERPRISE]: { value: }, + }, + }, + { + label: 'Step controls', + isContrast: false, + values: { + [SupportedPlansEnum.FREE]: { value: }, + [SupportedPlansEnum.BUSINESS]: { value: }, + [SupportedPlansEnum.ENTERPRISE]: { value: }, + }, + }, + { + label: 'Inbox', + isContrast: true, + isTitle: true, + values: { + [SupportedPlansEnum.FREE]: { value: '' }, + [SupportedPlansEnum.BUSINESS]: { value: '' }, + [SupportedPlansEnum.ENTERPRISE]: { value: '' }, + }, + }, + { + label: 'Inbox component', + isContrast: false, + values: { + [SupportedPlansEnum.FREE]: { value: }, + [SupportedPlansEnum.BUSINESS]: { value: }, + [SupportedPlansEnum.ENTERPRISE]: { value: }, + }, + }, + { + label: 'User preferences component', + isContrast: true, + values: { + [SupportedPlansEnum.FREE]: { value: }, + [SupportedPlansEnum.BUSINESS]: { value: }, + [SupportedPlansEnum.ENTERPRISE]: { value: }, + }, + }, + { + label: 'Remove Novu branding', + isContrast: false, + values: { + [SupportedPlansEnum.FREE]: { value: '-' }, + [SupportedPlansEnum.BUSINESS]: { value: }, + [SupportedPlansEnum.ENTERPRISE]: { value: }, + }, + }, + { + label: 'Notifications retention', + isContrast: true, + values: { + [SupportedPlansEnum.FREE]: { value: }, + [SupportedPlansEnum.BUSINESS]: { value: }, + [SupportedPlansEnum.ENTERPRISE]: { value: }, + }, + }, + { + label: 'Account administration and security', + isContrast: false, + isTitle: true, + values: { + [SupportedPlansEnum.FREE]: { value: '' }, + [SupportedPlansEnum.BUSINESS]: { value: '' }, + [SupportedPlansEnum.ENTERPRISE]: { value: '' }, + }, + }, + { + label: 'Team members', + isContrast: true, + values: { + [SupportedPlansEnum.FREE]: { value: }, + [SupportedPlansEnum.BUSINESS]: { value: }, + [SupportedPlansEnum.ENTERPRISE]: { value: }, + }, + }, + { + label: 'RBAC', + isContrast: false, + values: { + [SupportedPlansEnum.FREE]: { value: }, + [SupportedPlansEnum.BUSINESS]: { value: }, + [SupportedPlansEnum.ENTERPRISE]: { value: }, + }, + }, + { + label: 'GDPR compliance', + isContrast: true, + values: { + [SupportedPlansEnum.FREE]: { value: }, + [SupportedPlansEnum.BUSINESS]: { value: }, + [SupportedPlansEnum.ENTERPRISE]: { value: }, + }, + }, + { + label: 'SAML SSO and Enterprise SSO providers', + isContrast: false, + values: { + [SupportedPlansEnum.FREE]: { value: '-' }, + [SupportedPlansEnum.BUSINESS]: { value: '-' }, + [SupportedPlansEnum.ENTERPRISE]: { value: }, + }, + }, + { + label: 'Support and account management', + isContrast: true, + isTitle: true, + values: { + [SupportedPlansEnum.FREE]: { value: '' }, + [SupportedPlansEnum.BUSINESS]: { value: '' }, + [SupportedPlansEnum.ENTERPRISE]: { value: '' }, + }, + }, + { + label: 'Support SLA', + isContrast: false, + values: { + [SupportedPlansEnum.FREE]: { value: 'Community & Intercom' }, + [SupportedPlansEnum.BUSINESS]: { value: '48 hours' }, + [SupportedPlansEnum.ENTERPRISE]: { value: '24 hours' }, + }, + }, + { + label: 'Support channels', + isContrast: true, + values: { + [SupportedPlansEnum.FREE]: { value: 'Community & Intercom' }, + [SupportedPlansEnum.BUSINESS]: { value: 'Chat and Email' }, + [SupportedPlansEnum.ENTERPRISE]: { value: 'Dedicated support' }, + }, + }, + { + label: 'Legal & Vendor management', + isContrast: false, + isTitle: true, + values: { + [SupportedPlansEnum.FREE]: { value: '' }, + [SupportedPlansEnum.BUSINESS]: { value: '' }, + [SupportedPlansEnum.ENTERPRISE]: { value: '' }, + }, + }, + { + label: 'Payment method', + isContrast: true, + values: { + [SupportedPlansEnum.FREE]: { value: 'N/A' }, + [SupportedPlansEnum.BUSINESS]: { value: 'Credit card only' }, + [SupportedPlansEnum.ENTERPRISE]: { value: 'PO and Invoicing' }, + }, + }, + { + label: 'Terms of service', + isContrast: false, + values: { + [SupportedPlansEnum.FREE]: { value: 'Standard' }, + [SupportedPlansEnum.BUSINESS]: { value: 'Standard' }, + [SupportedPlansEnum.ENTERPRISE]: { value: 'Custom' }, + }, + }, + { + label: 'DPA', + isContrast: true, + values: { + [SupportedPlansEnum.FREE]: { value: 'Standard' }, + [SupportedPlansEnum.BUSINESS]: { value: 'Standard' }, + [SupportedPlansEnum.ENTERPRISE]: { value: 'Custom' }, + }, + }, + { + label: 'Security review', + isContrast: false, + values: { + [SupportedPlansEnum.FREE]: { value: 'SOC 2 and ISO 27001 upon request' }, + [SupportedPlansEnum.BUSINESS]: { value: 'Custom' }, + [SupportedPlansEnum.ENTERPRISE]: { value: 'Custom' }, + }, + }, +]; + +export const Features = () => { + return ( +
+ {features.map((feature, index) => ( + + ))} +
+ ); +}; + +const FeatureRow = ({ feature }: { feature: Feature }) => ( +
+ + + {feature.label} + + + + {Object.entries(feature.values).map(([plan, value]) => { + return {value.value}; + })} +
+); + +const styles = { + featureList: css({ + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + alignSelf: 'stretch', + }), + rowContainer: (isContrast: boolean | undefined, isHeader: boolean | undefined) => + css({ + display: 'flex', + alignItems: 'flex-start', + alignSelf: 'stretch', + background: isContrast ? 'surface.panel' : undefined, + borderTop: isHeader ? '1px solid #34343A !important' : undefined, + borderBottom: isHeader ? '1px solid #34343A !important' : undefined, + }), +}; diff --git a/apps/web/src/ee/billing/components/billingV2/HighlightsRow.tsx b/apps/web/src/ee/billing/components/billingV2/HighlightsRow.tsx new file mode 100644 index 00000000000..70f7d77be19 --- /dev/null +++ b/apps/web/src/ee/billing/components/billingV2/HighlightsRow.tsx @@ -0,0 +1,92 @@ +import { css } from '@novu/novui/css'; +import { Text } from '@novu/novui'; +import styled from '@emotion/styled'; +import { List } from '@mantine/core'; +import { ApiServiceLevelEnum } from '@novu/shared'; +import { Badge } from './Badge'; + +const Cell = styled.div` + display: flex; + padding: 24px; + align-items: flex-start; + flex: 1 0 0; + align-self: stretch; + flex-direction: column; + gap: 16px; +`; + +type Highlight = { + text: string; + badgeLabel?: string; +}; + +type PlanHighlights = { + [key in ApiServiceLevelEnum]?: Highlight[]; +}; + +const highlights: PlanHighlights = { + [ApiServiceLevelEnum.FREE]: [ + { text: 'Up to 30,000 events per month' }, + { text: '7 days activity feed' }, + { text: '3 teammates', badgeLabel: 'Coming soon' }, + ], + [ApiServiceLevelEnum.BUSINESS]: [ + { text: 'Up to 250,000 events per month' }, + { text: '90 days activity feed', badgeLabel: 'Coming soon' }, + { text: '50 teammates', badgeLabel: 'Coming soon' }, + { text: 'RBAC', badgeLabel: 'Coming soon' }, + ], + [ApiServiceLevelEnum.ENTERPRISE]: [ + { text: 'Up to 5,000,000 events per month' }, + { text: 'Custom time activity feed', badgeLabel: 'Coming soon' }, + { text: 'Unlimited teammates', badgeLabel: 'Coming soon' }, + { text: 'SAML SSO' }, + { text: 'RBAC', badgeLabel: 'Coming soon' }, + ], +}; + +const PlanHighlights = ({ planHighlights }: { planHighlights: Highlight[] }) => ( + + + {planHighlights.map((item, index) => ( + + + {item.text} {item.badgeLabel && } + + + ))} + + +); + +export const HighlightsRow = () => { + return ( +
+ + Highlights + + {Object.entries(highlights).map(([planName, planHighlights]) => ( + + ))} +
+ ); +}; + +const styles = { + container: css({ + display: 'flex', + alignItems: 'flex-start', + alignSelf: 'stretch', + background: 'surface.panel', + }), + list: { + item: css({ + lineHeight: '24px !important', + }), + }, + listBadgeItem: css({ + display: 'flex', + alignItems: 'center', + gap: '8px', + }), +}; diff --git a/apps/web/src/ee/billing/components/billingV2/Plan.tsx b/apps/web/src/ee/billing/components/billingV2/Plan.tsx new file mode 100644 index 00000000000..299f3395371 --- /dev/null +++ b/apps/web/src/ee/billing/components/billingV2/Plan.tsx @@ -0,0 +1,51 @@ +import { Center, Loader, useMantineTheme } from '@mantine/core'; +import { colors, errorMessage, successMessage } from '@novu/design-system'; +import { useEffect, useState } from 'react'; +import { PlanSwitcher } from './PlanSwitcher'; +import { useSubscriptionContext } from '../SubscriptionProvider'; +import { ActivePlanBanner } from './ActivePlanBanner'; +import { PlansRow } from './PlansRow'; +import { HighlightsRow } from './HighlightsRow'; +import { Features } from './Features'; + +export const Plan = () => { + const theme = useMantineTheme(); + const { isLoading, billingInterval: subscriptionBillingInterval } = useSubscriptionContext(); + const [selectedBillingInterval, setSelectedBillingInterval] = useState<'month' | 'year'>( + subscriptionBillingInterval || 'month' + ); + + useEffect(() => { + const checkoutResult = new URLSearchParams(window.location.search).get('result'); + + if (checkoutResult === 'success') { + successMessage('Payment was successful.'); + } + + if (checkoutResult === 'canceled') { + errorMessage('Order canceled.'); + } + }, []); + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( + <> + + + + + + + ); +}; diff --git a/apps/web/src/ee/billing/components/billingV2/PlanActionButton.tsx b/apps/web/src/ee/billing/components/billingV2/PlanActionButton.tsx new file mode 100644 index 00000000000..f8b968f0aea --- /dev/null +++ b/apps/web/src/ee/billing/components/billingV2/PlanActionButton.tsx @@ -0,0 +1,80 @@ +import { Button } from '@novu/novui'; +import { css } from '@novu/novui/css'; +import { ApiServiceLevelEnum } from '@novu/shared'; +import { useMutation } from '@tanstack/react-query'; +import { errorMessage, When } from '@novu/design-system'; +import { api } from '../../../../api'; +import { useSubscriptionContext } from '../SubscriptionProvider'; +import { useSegment } from '../../../../components/providers/SegmentProvider'; + +const checkoutUrl = '/v1/billing/checkout-session'; +const billingPortalUrl = '/v1/billing/portal'; + +export const PlanActionButton = ({ selectedBillingInterval }: { selectedBillingInterval: 'month' | 'year' }) => { + const segment = useSegment(); + const { isActive, trial, apiServiceLevel } = useSubscriptionContext(); + const isPaidSubscriptionActive = isActive && !trial.isActive && apiServiceLevel !== ApiServiceLevelEnum.FREE; + + const { mutateAsync: checkout, isLoading: isCheckingOut } = useMutation<{ stripeCheckoutUrl: string }, Error>( + () => + api.post(checkoutUrl, { + billingInterval: selectedBillingInterval, + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + }), + { + onSuccess: (data) => { + window.location.href = data.stripeCheckoutUrl; + }, + onError: (e: Error) => { + errorMessage(e.message || 'Unexpected error'); + }, + } + ); + + const { mutateAsync: goToPortal, isLoading: isGoingToPortal } = useMutation( + () => api.get(billingPortalUrl), + { + onSuccess: (url) => { + window.location.href = url; + }, + onError: (e: Error) => { + errorMessage(e.message || 'Unexpected error'); + }, + } + ); + + return ( + <> + + + + + + + + ); +}; + +const styles = { + planActionButton: css({ + background: '#2A92E7 !important', + fontWeight: '400 !important', + }), +}; diff --git a/apps/web/src/ee/billing/components/billingV2/PlanSwitcher.tsx b/apps/web/src/ee/billing/components/billingV2/PlanSwitcher.tsx new file mode 100644 index 00000000000..f64fc214b47 --- /dev/null +++ b/apps/web/src/ee/billing/components/billingV2/PlanSwitcher.tsx @@ -0,0 +1,93 @@ +import { MantineTheme, Tabs, useMantineTheme } from '@mantine/core'; +import { Title } from '@novu/novui'; +import { css } from '@novu/novui/css'; + +export const PlanSwitcher = ({ + theme, + selectedBillingInterval, + setSelectedBillingInterval, +}: { + theme: MantineTheme; + selectedBillingInterval: 'month' | 'year'; + setSelectedBillingInterval: (value: 'month' | 'year') => void; +}) => ( +
+ + All plans + +
+ + + + Monthly + + + Annually 10% off + + + +
+
+); + +const styles = { + planSwitcherContainer: (theme: MantineTheme) => + css({ + position: 'relative', + display: 'flex', + height: '40px', + justifyContent: 'space-between', + alignItems: 'flex-end', + alignSelf: 'stretch', + borderBottom: theme.colorScheme === 'dark' ? '1px solid #34343A' : '1px solid #e4e2e4ff', + }), + planHeaderTitle: css({ + position: 'absolute', + left: '0', + top: '0', + }), + tabsWrapper: css({ + flex: 1, + display: 'flex', + justifyContent: 'center', + }), + tabs: { + tabsList: css({ + display: 'flex', + justifyContent: 'center', + alignItems: 'flex-end', + flex: '1 0 0', + borderBottom: 'none !important', + }), + }, + tab: (theme: MantineTheme) => + css({ + padding: '12px !important', + gap: '8px !important', + color: 'typography.text.secondary !important', + fontSize: '14px !important', + lineHeight: '16px !important', + '& span': { + color: 'mode.local.middle', + }, + '&[data-active="true"]': { + color: 'typography.text.main !important', + borderBottom: theme.colorScheme === 'dark' ? '1px solid #FFF !important' : '1px solid #e4e2e4ff !important', + fontWeight: '600 !important', + + '& span': { + color: 'mode.local.middle', + fontWeight: '600', + }, + }, + '&:hover': { + color: 'typography.text.main !important', + backgroundColor: 'transparent !important', + + '& span': { + color: 'mode.local.middle', + fontWeight: '600', + }, + }, + }), +}; diff --git a/apps/web/src/ee/billing/components/billingV2/PlansRow.tsx b/apps/web/src/ee/billing/components/billingV2/PlansRow.tsx new file mode 100644 index 00000000000..3a41df599da --- /dev/null +++ b/apps/web/src/ee/billing/components/billingV2/PlansRow.tsx @@ -0,0 +1,91 @@ +import { css } from '@novu/novui/css'; +import { Title, Text } from '@novu/novui'; +import styled from '@emotion/styled'; +import { MantineTheme } from '@mantine/core'; +import { Badge } from './Badge'; +import { PlanActionButton } from './PlanActionButton'; +import { ContactUsButton } from './ContactUsButton'; + +const Cell = styled.div` + display: flex; + padding: 24px; + align-items: flex-start; + flex: 1 0 0; + align-self: stretch; + flex-direction: column; + gap: 16px; +`; + +const PriceDisplay = ({ price, subtitle, events }) => ( +
+
+ {price} + {subtitle} +
+ {events} +
+); + +export const PlansRow = ({ + theme, + selectedBillingInterval, +}: { + theme: MantineTheme; + selectedBillingInterval: 'month' | 'year'; +}) => { + const businessPlanPrice = selectedBillingInterval === 'year' ? '$2,700' : '$250'; + + return ( +
+ + + Plans + + + + + Free + + + + + + Business <Badge label="Popular" /> + + + + + + + Enterprise + + Custom pricing, billing, and extended services. + + +
+ ); +}; + +const styles = { + container: (theme: MantineTheme) => + css({ + display: 'flex', + alignItems: 'flex-start', + alignSelf: 'stretch', + borderBottom: theme.colorScheme === 'dark' ? '1px solid #34343A' : '1px solid #e4e2e4ff', + }), + + priceDisplay: css({ + display: 'flex', + alignItems: 'flex-end', + gap: '4px', + }), +}; diff --git a/apps/web/src/ee/billing/components/billingV2/UsageProgress.tsx b/apps/web/src/ee/billing/components/billingV2/UsageProgress.tsx index e64a6cf0353..c349e0eff57 100644 --- a/apps/web/src/ee/billing/components/billingV2/UsageProgress.tsx +++ b/apps/web/src/ee/billing/components/billingV2/UsageProgress.tsx @@ -14,7 +14,7 @@ export const UsageProgress = ({ apiServiceLevel, currentEvents, maxEvents }) =>
0 - {maxEvents.toLocaleString()} + {maxEvents?.toLocaleString()}
); diff --git a/apps/web/src/ee/billing/hooks/useSubscription.ts b/apps/web/src/ee/billing/hooks/useSubscription.ts index acae2324485..9374df0271d 100644 --- a/apps/web/src/ee/billing/hooks/useSubscription.ts +++ b/apps/web/src/ee/billing/hooks/useSubscription.ts @@ -1,9 +1,9 @@ import { useQuery } from '@tanstack/react-query'; import { useMemo } from 'react'; import { differenceInDays, isSameDay } from 'date-fns'; +import { ApiServiceLevelEnum, GetSubscriptionDto } from '@novu/shared'; import { useAuth } from '../../../hooks/useAuth'; import { api } from '../../../api'; -import { ApiServiceLevelEnum, GetSubscriptionDto } from '@novu/shared'; const today = new Date(); @@ -36,6 +36,16 @@ export const useSubscription = () => { daysTotal: 0, }, }, + select: (data) => { + return { + ...data, + events: { + ...data.events, + // if included is null, customer doesn't have a valid metered subscription, default to 0 + included: data.events.included ?? 0, + }, + }; + }, } ); diff --git a/apps/web/src/ee/billing/pages/BillingPage.tsx b/apps/web/src/ee/billing/pages/BillingPage.tsx index 05d9ac4cfcd..2227c497c24 100644 --- a/apps/web/src/ee/billing/pages/BillingPage.tsx +++ b/apps/web/src/ee/billing/pages/BillingPage.tsx @@ -1,19 +1,19 @@ import React, { useEffect } from 'react'; +import { FeatureFlagsKeysEnum } from '@novu/shared'; import { Plan } from '../components/Plan'; +import { Plan as PlanV2 } from '../components/billingV2/Plan'; import { SubscriptionProvider } from '../components/SubscriptionProvider'; import { useSegment } from '../../../components/providers/SegmentProvider'; +import { useFeatureFlag } from '../../../hooks/useFeatureFlag'; export const BillingPage = () => { const segment = useSegment(); + const isImprovedBillingEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_IMPROVED_BILLING_ENABLED); useEffect(() => { segment.track('Billing Page Viewed'); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - return ( - - - - ); + return {isImprovedBillingEnabled ? : }; }; diff --git a/apps/web/src/ee/clerk/pages/ManageAccountPage.styles.ts b/apps/web/src/ee/clerk/pages/ManageAccountPage.styles.ts index 92fa99cca5e..79de1afaf0f 100644 --- a/apps/web/src/ee/clerk/pages/ManageAccountPage.styles.ts +++ b/apps/web/src/ee/clerk/pages/ManageAccountPage.styles.ts @@ -80,7 +80,7 @@ export const tabsStyles = { panel: css({ padding: '150', borderRadius: 'l', - backgroundColor: 'surface.panelSubsection', + backgroundColor: 'surface.page', overflowY: 'auto', }), tabsList: css({ diff --git a/apps/web/src/ee/clerk/providers/ClerkProvider.tsx b/apps/web/src/ee/clerk/providers/ClerkProvider.tsx index ccff0310dee..d28b59e55a9 100644 --- a/apps/web/src/ee/clerk/providers/ClerkProvider.tsx +++ b/apps/web/src/ee/clerk/providers/ClerkProvider.tsx @@ -190,7 +190,7 @@ const CLERK_MODAL_ELEMENT = { width: '100%', }, scrollBox: { - backgroundColor: 'var(--nv-colors-surface-panel-subsection)', + backgroundColor: 'surface.page', padding: '0', }, pageScrollBox: { diff --git a/packages/js/package.json b/packages/js/package.json index 34f852eeb2e..a28bd70741c 100644 --- a/packages/js/package.json +++ b/packages/js/package.json @@ -1,6 +1,6 @@ { "name": "@novu/js", - "version": "2.3.0", + "version": "2.3.1", "repository": "https://github.com/novuhq/novu", "description": "Novu's JavaScript SDK for building custom inbox notification experiences", "author": "", diff --git a/packages/react/package.json b/packages/react/package.json index 63a405e5ae4..6f141680f1b 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@novu/react", - "version": "2.3.0", + "version": "2.3.1", "repository": "https://github.com/novuhq/novu", "description": "Novu's React SDK for building custom inbox notification experiences", "author": "",