You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

plan-item.tsx 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. 'use client'
  2. import type { FC, ReactNode } from 'react'
  3. import React from 'react'
  4. import { useTranslation } from 'react-i18next'
  5. import { RiApps2Line, RiBook2Line, RiBrain2Line, RiChatAiLine, RiFileEditLine, RiFolder6Line, RiGroupLine, RiHardDrive3Line, RiHistoryLine, RiProgress3Line, RiQuestionLine, RiSeoLine, RiTerminalBoxLine } from '@remixicon/react'
  6. import type { BasicPlan } from '../type'
  7. import { Plan } from '../type'
  8. import { ALL_PLANS, NUM_INFINITE } from '../config'
  9. import Toast from '../../base/toast'
  10. import Tooltip from '../../base/tooltip'
  11. import Divider from '../../base/divider'
  12. import { ArCube1, Group2, Keyframe, SparklesSoft } from '../../base/icons/src/public/billing'
  13. import { PlanRange } from './select-plan-range'
  14. import cn from '@/utils/classnames'
  15. import { useAppContext } from '@/context/app-context'
  16. import { fetchSubscriptionUrls } from '@/service/billing'
  17. type Props = {
  18. currentPlan: BasicPlan
  19. plan: BasicPlan
  20. planRange: PlanRange
  21. canPay: boolean
  22. }
  23. const KeyValue = ({ icon, label, tooltip }: { icon: ReactNode; label: string; tooltip?: ReactNode }) => {
  24. return (
  25. <div className='flex text-text-tertiary'>
  26. <div className='flex size-4 items-center justify-center'>
  27. {icon}
  28. </div>
  29. <div className='system-sm-regular ml-2 mr-0.5 text-text-primary'>{label}</div>
  30. {tooltip && (
  31. <Tooltip
  32. asChild
  33. popupContent={tooltip}
  34. popupClassName='w-[200px]'
  35. >
  36. <div className='flex size-4 items-center justify-center'>
  37. <RiQuestionLine className='text-text-quaternary' />
  38. </div>
  39. </Tooltip>
  40. )}
  41. </div>
  42. )
  43. }
  44. const priceClassName = 'leading-[125%] text-[28px] font-bold text-text-primary'
  45. const style = {
  46. [Plan.sandbox]: {
  47. icon: <ArCube1 className='size-7 text-text-primary' />,
  48. description: 'text-util-colors-gray-gray-600',
  49. btnStyle: 'bg-components-button-secondary-bg hover:bg-components-button-secondary-bg-hover border-[0.5px] border-components-button-secondary-border text-text-primary',
  50. btnDisabledStyle: 'bg-components-button-secondary-bg-disabled hover:bg-components-button-secondary-bg-disabled border-components-button-secondary-border-disabled text-components-button-secondary-text-disabled',
  51. },
  52. [Plan.professional]: {
  53. icon: <Keyframe className='size-7 text-util-colors-blue-brand-blue-brand-600' />,
  54. description: 'text-util-colors-blue-brand-blue-brand-600',
  55. btnStyle: 'bg-components-button-primary-bg hover:bg-components-button-primary-bg-hover border border-components-button-primary-border text-components-button-primary-text',
  56. btnDisabledStyle: 'bg-components-button-primary-bg-disabled hover:bg-components-button-primary-bg-disabled border-components-button-primary-border-disabled text-components-button-primary-text-disabled',
  57. },
  58. [Plan.team]: {
  59. icon: <Group2 className='size-7 text-util-colors-indigo-indigo-600' />,
  60. description: 'text-util-colors-indigo-indigo-600',
  61. btnStyle: 'bg-components-button-indigo-bg hover:bg-components-button-indigo-bg-hover border border-components-button-primary-border text-components-button-primary-text',
  62. btnDisabledStyle: 'bg-components-button-indigo-bg-disabled hover:bg-components-button-indigo-bg-disabled border-components-button-indigo-border-disabled text-components-button-primary-text-disabled',
  63. },
  64. }
  65. const PlanItem: FC<Props> = ({
  66. plan,
  67. currentPlan,
  68. planRange,
  69. }) => {
  70. const { t } = useTranslation()
  71. const [loading, setLoading] = React.useState(false)
  72. const i18nPrefix = `billing.plans.${plan}`
  73. const isFreePlan = plan === Plan.sandbox
  74. const isMostPopularPlan = plan === Plan.professional
  75. const planInfo = ALL_PLANS[plan]
  76. const isYear = planRange === PlanRange.yearly
  77. const isCurrent = plan === currentPlan
  78. const isPlanDisabled = planInfo.level <= ALL_PLANS[currentPlan].level
  79. const { isCurrentWorkspaceManager } = useAppContext()
  80. const btnText = (() => {
  81. if (isCurrent)
  82. return t('billing.plansCommon.currentPlan')
  83. return ({
  84. [Plan.sandbox]: t('billing.plansCommon.startForFree'),
  85. [Plan.professional]: t('billing.plansCommon.getStarted'),
  86. [Plan.team]: t('billing.plansCommon.getStarted'),
  87. })[plan]
  88. })()
  89. const handleGetPayUrl = async () => {
  90. if (loading)
  91. return
  92. if (isPlanDisabled)
  93. return
  94. if (isFreePlan)
  95. return
  96. // Only workspace manager can buy plan
  97. if (!isCurrentWorkspaceManager) {
  98. Toast.notify({
  99. type: 'error',
  100. message: t('billing.buyPermissionDeniedTip'),
  101. className: 'z-[1001]',
  102. })
  103. return
  104. }
  105. setLoading(true)
  106. try {
  107. const res = await fetchSubscriptionUrls(plan, isYear ? 'year' : 'month')
  108. // Adb Block additional tracking block the gtag, so we need to redirect directly
  109. window.location.href = res.url
  110. }
  111. finally {
  112. setLoading(false)
  113. }
  114. }
  115. return (
  116. <div className={cn('flex w-[373px] flex-col rounded-2xl border-[0.5px] border-effects-highlight-lightmode-off bg-background-section-burn p-6',
  117. isMostPopularPlan ? 'border-effects-highlight shadow-lg backdrop-blur-[5px]' : 'hover:border-effects-highlight hover:shadow-lg hover:backdrop-blur-[5px]',
  118. )}>
  119. <div className='flex flex-col gap-y-1'>
  120. {style[plan].icon}
  121. <div className='flex items-center'>
  122. <div className='grow text-lg font-semibold uppercase leading-[125%] text-text-primary'>{t(`${i18nPrefix}.name`)}</div>
  123. {isMostPopularPlan && <div className='ml-1 flex shrink-0 items-center justify-center rounded-full border-[0.5px] bg-price-premium-badge-background px-1 py-[3px] text-components-premium-badge-grey-text-stop-0 shadow-xs'>
  124. <div className='pl-0.5'>
  125. <SparklesSoft className='size-3' />
  126. </div>
  127. <span className='system-2xs-semibold-uppercase bg-price-premium-text-background bg-clip-text px-0.5 text-transparent'>{t('billing.plansCommon.mostPopular')}</span>
  128. </div>}
  129. </div>
  130. <div className={cn(style[plan].description, 'system-sm-regular')}>{t(`${i18nPrefix}.description`)}</div>
  131. </div>
  132. <div className='my-5'>
  133. {/* Price */}
  134. {isFreePlan && (
  135. <div className={priceClassName}>{t('billing.plansCommon.free')}</div>
  136. )}
  137. {!isFreePlan && (
  138. <div className='flex items-end'>
  139. <div className={priceClassName}>${isYear ? planInfo.price * 10 : planInfo.price}</div>
  140. <div className='ml-1 flex flex-col'>
  141. {isYear && <div className='text-[14px] font-normal italic leading-[14px] text-text-warning'>{t('billing.plansCommon.save')}${planInfo.price * 2}</div>}
  142. <div className='text-[14px] font-normal leading-normal text-text-tertiary'>
  143. {t('billing.plansCommon.priceTip')}
  144. {t(`billing.plansCommon.${!isYear ? 'month' : 'year'}`)}</div>
  145. </div>
  146. </div>
  147. )}
  148. </div>
  149. <div
  150. className={cn('flex h-[42px] items-center justify-center rounded-full px-5 py-3',
  151. style[plan].btnStyle,
  152. isPlanDisabled && style[plan].btnDisabledStyle,
  153. isPlanDisabled ? 'cursor-not-allowed' : 'cursor-pointer')}
  154. onClick={handleGetPayUrl}
  155. >
  156. {btnText}
  157. </div>
  158. <div className='mt-6 flex flex-col gap-y-3'>
  159. <KeyValue
  160. icon={<RiChatAiLine />}
  161. label={isFreePlan
  162. ? t('billing.plansCommon.messageRequest.title', { count: planInfo.messageRequest })
  163. : t('billing.plansCommon.messageRequest.titlePerMonth', { count: planInfo.messageRequest })}
  164. tooltip={t('billing.plansCommon.messageRequest.tooltip') as string}
  165. />
  166. <KeyValue
  167. icon={<RiBrain2Line />}
  168. label={t('billing.plansCommon.modelProviders')}
  169. />
  170. <KeyValue
  171. icon={<RiFolder6Line />}
  172. label={t('billing.plansCommon.teamWorkspace', { count: planInfo.teamWorkspace })}
  173. />
  174. <KeyValue
  175. icon={<RiGroupLine />}
  176. label={t('billing.plansCommon.teamMember', { count: planInfo.teamMembers })}
  177. />
  178. <KeyValue
  179. icon={<RiApps2Line />}
  180. label={t('billing.plansCommon.buildApps', { count: planInfo.buildApps })}
  181. />
  182. <Divider bgStyle='gradient' />
  183. <KeyValue
  184. icon={<RiBook2Line />}
  185. label={t('billing.plansCommon.documents', { count: planInfo.documents })}
  186. tooltip={t('billing.plansCommon.documentsTooltip') as string}
  187. />
  188. <KeyValue
  189. icon={<RiHardDrive3Line />}
  190. label={t('billing.plansCommon.vectorSpace', { size: planInfo.vectorSpace })}
  191. tooltip={t('billing.plansCommon.vectorSpaceTooltip') as string}
  192. />
  193. <KeyValue
  194. icon={<RiSeoLine />}
  195. label={t('billing.plansCommon.documentsRequestQuota', { count: planInfo.documentsRequestQuota })}
  196. tooltip={t('billing.plansCommon.documentsRequestQuotaTooltip')}
  197. />
  198. <KeyValue
  199. icon={<RiTerminalBoxLine />}
  200. label={
  201. planInfo.apiRateLimit === NUM_INFINITE ? `${t('billing.plansCommon.unlimitedApiRate')}`
  202. : `${t('billing.plansCommon.apiRateLimitUnit', { count: planInfo.apiRateLimit })} ${t('billing.plansCommon.apiRateLimit')}`
  203. }
  204. tooltip={planInfo.apiRateLimit === NUM_INFINITE ? null : t('billing.plansCommon.apiRateLimitTooltip') as string}
  205. />
  206. <KeyValue
  207. icon={<RiProgress3Line />}
  208. label={[t(`billing.plansCommon.priority.${planInfo.documentProcessingPriority}`), t('billing.plansCommon.documentProcessingPriority')].join('')}
  209. />
  210. <Divider bgStyle='gradient' />
  211. <KeyValue
  212. icon={<RiFileEditLine />}
  213. label={t('billing.plansCommon.annotatedResponse.title', { count: planInfo.annotatedResponse })}
  214. tooltip={t('billing.plansCommon.annotatedResponse.tooltip') as string}
  215. />
  216. <KeyValue
  217. icon={<RiHistoryLine />}
  218. label={t('billing.plansCommon.logsHistory', { days: planInfo.logHistory === NUM_INFINITE ? t('billing.plansCommon.unlimited') as string : `${planInfo.logHistory} ${t('billing.plansCommon.days')}` })}
  219. />
  220. </div>
  221. </div>
  222. )
  223. }
  224. export default React.memo(PlanItem)