Skip to content

Commit 8108333

Browse files
authored
Merge pull request #470 from Swetrix/improvement/billing-in-user-settings
Move Billing to user settings
2 parents 17090d4 + 54c04c9 commit 8108333

File tree

11 files changed

+789
-777
lines changed

11 files changed

+789
-777
lines changed

web/app/components/pricing/BillingPricing.tsx

Lines changed: 51 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { Label, Radio, RadioGroup } from '@headlessui/react'
2-
import cx from 'clsx'
31
import dayjs from 'dayjs'
42
import _includes from 'lodash/includes'
53
import _isNil from 'lodash/isNil'
64
import _map from 'lodash/map'
5+
import _round from 'lodash/round'
76
import { QuestionIcon } from '@phosphor-icons/react'
87
import React, { memo, useEffect, useMemo, useState } from 'react'
98
import { Trans, useTranslation } from 'react-i18next'
@@ -22,11 +21,12 @@ import {
2221
import { Metainfo, DEFAULT_METAINFO } from '~/lib/models/Metainfo'
2322
import { useAuth } from '~/providers/AuthProvider'
2423
import { useTheme } from '~/providers/ThemeProvider'
25-
import type { BillingActionData } from '~/routes/billing'
24+
import type { UserSettingsActionData } from '~/routes/user-settings'
2625
import { Badge } from '~/ui/Badge'
2726
import Button from '~/ui/Button'
2827
import Loader from '~/ui/Loader'
2928
import Modal from '~/ui/Modal'
29+
import { Switch } from '~/ui/Switch'
3030
import Tooltip from '~/ui/Tooltip'
3131
import { cn } from '~/utils/generic'
3232
import routes from '~/utils/routes'
@@ -50,8 +50,8 @@ const BillingPricing = ({
5050
const { isAuthenticated, user, loadUser } = useAuth()
5151
const { theme } = useTheme()
5252

53-
const previewFetcher = useFetcher<BillingActionData>()
54-
const changePlanFetcher = useFetcher<BillingActionData>()
53+
const previewFetcher = useFetcher<UserSettingsActionData>()
54+
const changePlanFetcher = useFetcher<UserSettingsActionData>()
5555

5656
const [planCodeLoading, setPlanCodeLoading] = useState<string | null>(null)
5757
const [
@@ -174,7 +174,7 @@ const BillingPricing = ({
174174
setIsNewPlanConfirmationModalOpened(true)
175175
previewFetcher.submit(
176176
{ intent: 'preview-subscription-update', planId: String(planId) },
177-
{ method: 'POST', action: '/billing' },
177+
{ method: 'POST', action: '/user-settings' },
178178
)
179179
}
180180

@@ -230,7 +230,7 @@ const BillingPricing = ({
230230
const updateSubscription = () => {
231231
changePlanFetcher.submit(
232232
{ intent: 'change-subscription-plan', planId: String(newPlanId) },
233-
{ method: 'POST', action: '/billing' },
233+
{ method: 'POST', action: '/user-settings' },
234234
)
235235
}
236236

@@ -292,48 +292,59 @@ const BillingPricing = ({
292292
return validCodes.map((code) => PLAN_LIMITS[code])
293293
}, [PLAN_CODES_ARRAY])
294294

295+
const yearlyDiscount = useMemo(() => {
296+
const firstTier = tiers[0]
297+
if (!firstTier) return 0
298+
299+
const monthlyPrice = firstTier.price[currencyCode]?.monthly ?? 0
300+
const yearlyPrice = firstTier.price[currencyCode]?.yearly ?? 0
301+
302+
if (monthlyPrice === 0) return 0
303+
304+
const annualCostIfMonthly = monthlyPrice * 12
305+
const savings = annualCostIfMonthly - yearlyPrice
306+
const discountPercentage = (savings / annualCostIfMonthly) * 100
307+
308+
return _round(discountPercentage, 0)
309+
}, [tiers, currencyCode])
310+
295311
return (
296312
<>
297313
<div className='rounded-xl border border-gray-200 p-4 sm:p-6 dark:border-white/10'>
298-
<div className='mb-3 flex justify-between'>
314+
<div className='mb-3 flex items-center justify-between'>
299315
<h2 className='text-2xl font-bold text-black dark:text-gray-50'>
300316
{t('common.billing')}
301317
</h2>
302-
<RadioGroup
303-
value={billingFrequency}
304-
onChange={setBillingFrequency}
305-
className='grid grid-cols-2 gap-x-1 rounded-md p-1 text-center text-xs leading-5 font-semibold ring-1 ring-gray-200 dark:ring-white/20'
318+
<button
319+
type='button'
320+
onClick={() =>
321+
setBillingFrequency(
322+
billingFrequency === BillingFrequency.yearly
323+
? BillingFrequency.monthly
324+
: BillingFrequency.yearly,
325+
)
326+
}
327+
className='flex cursor-pointer items-center gap-2 rounded-lg bg-gray-100 px-3 py-2 transition-colors hover:bg-gray-200 dark:bg-slate-800 dark:hover:bg-slate-700'
306328
>
307-
<Label className='sr-only'>{t('pricing.frequency')}</Label>
308-
<Radio
309-
key={BillingFrequency.monthly}
310-
value={BillingFrequency.monthly}
311-
className={({ checked }) =>
312-
cx(
313-
checked
314-
? 'bg-slate-900 text-gray-50 dark:bg-white/90 dark:text-slate-900'
315-
: 'text-slate-700 hover:bg-slate-200 dark:text-gray-200 dark:hover:bg-white/30 dark:hover:text-white',
316-
'flex cursor-pointer items-center justify-center rounded-md px-2.5 py-1 transition-all',
317-
)
318-
}
319-
>
320-
<span>{t('pricing.monthlyBilling')}</span>
321-
</Radio>
322-
<Radio
323-
key={BillingFrequency.yearly}
324-
value={BillingFrequency.yearly}
325-
className={({ checked }) =>
326-
cx(
327-
checked
328-
? 'bg-slate-900 text-gray-50 dark:bg-white/90 dark:text-slate-900'
329-
: 'text-slate-700 hover:bg-slate-200 dark:text-gray-200 dark:hover:bg-white/30 dark:hover:text-white',
330-
'flex cursor-pointer items-center justify-center rounded-md px-2.5 py-1 transition-all',
329+
<span className='text-sm font-medium text-gray-700 dark:text-gray-200'>
330+
{t('pricing.billedYearly')}
331+
</span>
332+
{yearlyDiscount > 0 ? (
333+
<span className='rounded-md bg-emerald-100 px-1.5 py-0.5 text-xs font-semibold text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-400'>
334+
-{yearlyDiscount} %
335+
</span>
336+
) : null}
337+
<Switch
338+
checked={billingFrequency === BillingFrequency.yearly}
339+
onChange={() =>
340+
setBillingFrequency(
341+
billingFrequency === BillingFrequency.yearly
342+
? BillingFrequency.monthly
343+
: BillingFrequency.yearly,
331344
)
332345
}
333-
>
334-
<span>{t('pricing.yearlyBilling')}</span>
335-
</Radio>
336-
</RadioGroup>
346+
/>
347+
</button>
337348
</div>
338349

339350
<div className='space-y-2'>

web/app/components/pricing/MarketingPricing.tsx

Lines changed: 46 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import { Label, Radio, RadioGroup } from '@headlessui/react'
2-
import cx from 'clsx'
31
import _map from 'lodash/map'
2+
import _round from 'lodash/round'
43
import { ArrowRightIcon, QuestionIcon, CheckIcon } from '@phosphor-icons/react'
5-
import { useState } from 'react'
4+
import { useState, useMemo } from 'react'
65
import { useTranslation } from 'react-i18next'
76
import { Link } from 'react-router'
87

@@ -15,6 +14,7 @@ import {
1514
} from '~/lib/constants'
1615
import { DEFAULT_METAINFO, Metainfo } from '~/lib/models/Metainfo'
1716
import { useAuth } from '~/providers/AuthProvider'
17+
import { Switch } from '~/ui/Switch'
1818
import Tooltip from '~/ui/Tooltip'
1919
import routes from '~/utils/routes'
2020

@@ -40,6 +40,22 @@ const MarketingPricing = ({
4040
(code) => PLAN_LIMITS[code as keyof typeof PLAN_LIMITS],
4141
)
4242

43+
const yearlyDiscount = useMemo(() => {
44+
const firstPlan = plans[0]
45+
if (!firstPlan) return 0
46+
47+
const monthlyPrice = firstPlan.price[currencyCode]?.monthly ?? 0
48+
const yearlyPrice = firstPlan.price[currencyCode]?.yearly ?? 0
49+
50+
if (monthlyPrice === 0) return 0
51+
52+
const annualCostIfMonthly = monthlyPrice * 12
53+
const savings = annualCostIfMonthly - yearlyPrice
54+
const discountPercentage = (savings / annualCostIfMonthly) * 100
55+
56+
return _round(discountPercentage, 0)
57+
}, [plans, currencyCode])
58+
4359
return (
4460
<section id='pricing' className='relative p-2'>
4561
<div className='rounded-xl bg-slate-900 py-16 sm:py-20'>
@@ -142,41 +158,36 @@ const MarketingPricing = ({
142158

143159
<div className='col-span-12 lg:col-span-7'>
144160
<div className='mb-3 flex justify-end'>
145-
<RadioGroup
146-
value={billingFrequency}
147-
onChange={setBillingFrequency}
148-
className='grid grid-cols-2 gap-x-1 rounded-md p-1 text-center text-xs leading-5 font-semibold ring-1 ring-white/20'
161+
<button
162+
type='button'
163+
onClick={() =>
164+
setBillingFrequency(
165+
billingFrequency === BillingFrequency.yearly
166+
? BillingFrequency.monthly
167+
: BillingFrequency.yearly,
168+
)
169+
}
170+
className='flex cursor-pointer items-center gap-2 rounded-lg bg-white/10 px-3 py-2 transition-colors hover:bg-white/20'
149171
>
150-
<Label className='sr-only'>{t('pricing.frequency')}</Label>
151-
<Radio
152-
key={BillingFrequency.monthly}
153-
value={BillingFrequency.monthly}
154-
className={({ checked }) =>
155-
cx(
156-
checked
157-
? 'bg-white/90 text-slate-900'
158-
: 'text-gray-200 hover:bg-white/30 hover:text-white',
159-
'flex cursor-pointer items-center justify-center rounded-md px-2.5 py-1 transition-all',
160-
)
161-
}
162-
>
163-
<span>{t('pricing.monthlyBilling')}</span>
164-
</Radio>
165-
<Radio
166-
key={BillingFrequency.yearly}
167-
value={BillingFrequency.yearly}
168-
className={({ checked }) =>
169-
cx(
170-
checked
171-
? 'bg-white/90 text-slate-900'
172-
: 'text-gray-200 hover:bg-white/30 hover:text-white',
173-
'flex cursor-pointer items-center justify-center rounded-md px-2.5 py-1 transition-all',
172+
<span className='text-sm font-medium text-gray-100'>
173+
{t('pricing.billedYearly')}
174+
</span>
175+
{yearlyDiscount > 0 ? (
176+
<span className='rounded-md bg-emerald-500/20 px-1.5 py-0.5 text-xs font-semibold text-emerald-300'>
177+
-{yearlyDiscount} %
178+
</span>
179+
) : null}
180+
<Switch
181+
checked={billingFrequency === BillingFrequency.yearly}
182+
onChange={() =>
183+
setBillingFrequency(
184+
billingFrequency === BillingFrequency.yearly
185+
? BillingFrequency.monthly
186+
: BillingFrequency.yearly,
174187
)
175188
}
176-
>
177-
<span>{t('pricing.yearlyBilling')}</span>
178-
</Radio>
179-
</RadioGroup>
189+
/>
190+
</button>
180191
</div>
181192
<div className='space-y-3'>
182193
{_map(plans, (tier) => (

0 commit comments

Comments
 (0)