1- import { Label , Radio , RadioGroup } from '@headlessui/react'
2- import cx from 'clsx'
31import dayjs from 'dayjs'
42import _includes from 'lodash/includes'
53import _isNil from 'lodash/isNil'
64import _map from 'lodash/map'
5+ import _round from 'lodash/round'
76import { QuestionIcon } from '@phosphor-icons/react'
87import React , { memo , useEffect , useMemo , useState } from 'react'
98import { Trans , useTranslation } from 'react-i18next'
@@ -22,11 +21,12 @@ import {
2221import { Metainfo , DEFAULT_METAINFO } from '~/lib/models/Metainfo'
2322import { useAuth } from '~/providers/AuthProvider'
2423import { useTheme } from '~/providers/ThemeProvider'
25- import type { BillingActionData } from '~/routes/billing '
24+ import type { UserSettingsActionData } from '~/routes/user-settings '
2625import { Badge } from '~/ui/Badge'
2726import Button from '~/ui/Button'
2827import Loader from '~/ui/Loader'
2928import Modal from '~/ui/Modal'
29+ import { Switch } from '~/ui/Switch'
3030import Tooltip from '~/ui/Tooltip'
3131import { cn } from '~/utils/generic'
3232import 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' >
0 commit comments