plan-item.tsx 9.6 KB

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