mirror of
https://github.com/langgenius/dify.git
synced 2025-12-19 17:27:16 -05:00
feat: make billing management entry prominent and enable current plan portal (#29321)
This commit is contained in:
@@ -2,36 +2,61 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useSWR from 'swr'
|
||||
import {
|
||||
RiArrowRightUpLine,
|
||||
} from '@remixicon/react'
|
||||
import PlanComp from '../plan'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { fetchBillingUrl } from '@/service/billing'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useBillingUrl } from '@/service/use-billing'
|
||||
|
||||
const Billing: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
const { enableBilling } = useProviderContext()
|
||||
const { data: billingUrl } = useSWR(
|
||||
(!enableBilling || !isCurrentWorkspaceManager) ? null : ['/billing/invoices'],
|
||||
() => fetchBillingUrl().then(data => data.url),
|
||||
)
|
||||
const { data: billingUrl, isFetching, refetch } = useBillingUrl(enableBilling && isCurrentWorkspaceManager)
|
||||
|
||||
const handleOpenBilling = async () => {
|
||||
// Open synchronously to preserve user gesture for popup blockers
|
||||
if (billingUrl) {
|
||||
window.open(billingUrl, '_blank', 'noopener,noreferrer')
|
||||
return
|
||||
}
|
||||
|
||||
const newWindow = window.open('', '_blank', 'noopener,noreferrer')
|
||||
try {
|
||||
const url = (await refetch()).data
|
||||
if (url && newWindow) {
|
||||
newWindow.location.href = url
|
||||
return
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error('Failed to fetch billing url', err)
|
||||
}
|
||||
// Close the placeholder window if we failed to fetch the URL
|
||||
newWindow?.close()
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PlanComp loc={'billing-page'} />
|
||||
{enableBilling && isCurrentWorkspaceManager && billingUrl && (
|
||||
<>
|
||||
<Divider className='my-4' />
|
||||
<a className='system-xs-medium flex cursor-pointer items-center text-text-accent-light-mode-only' href={billingUrl} target='_blank' rel='noopener noreferrer'>
|
||||
<span className='pr-0.5'>{t('billing.viewBilling')}</span>
|
||||
{enableBilling && isCurrentWorkspaceManager && (
|
||||
<button
|
||||
type='button'
|
||||
className='mt-3 flex w-full items-center justify-between rounded-xl bg-background-section-burn px-4 py-3'
|
||||
onClick={handleOpenBilling}
|
||||
disabled={isFetching}
|
||||
>
|
||||
<div className='flex flex-col gap-0.5 text-left'>
|
||||
<div className='system-md-semibold text-text-primary'>{t('billing.viewBillingTitle')}</div>
|
||||
<div className='system-sm-regular text-text-secondary'>{t('billing.viewBillingDescription')}</div>
|
||||
</div>
|
||||
<span className='inline-flex h-8 w-24 items-center justify-center gap-0.5 rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-3 py-2 text-saas-dify-blue-accessible shadow-[0_1px_2px_rgba(9,9,11,0.05)] backdrop-blur-[5px]'>
|
||||
<span className='system-sm-medium leading-[1]'>{t('billing.viewBillingAction')}</span>
|
||||
<RiArrowRightUpLine className='h-4 w-4' />
|
||||
</a>
|
||||
</>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ALL_PLANS } from '../../../config'
|
||||
import Toast from '../../../../base/toast'
|
||||
import { PlanRange } from '../../plan-switcher/plan-range-switcher'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { fetchSubscriptionUrls } from '@/service/billing'
|
||||
import { fetchBillingUrl, fetchSubscriptionUrls } from '@/service/billing'
|
||||
import List from './list'
|
||||
import Button from './button'
|
||||
import { Professional, Sandbox, Team } from '../../assets'
|
||||
@@ -39,7 +39,8 @@ const CloudPlanItem: FC<CloudPlanItemProps> = ({
|
||||
const planInfo = ALL_PLANS[plan]
|
||||
const isYear = planRange === PlanRange.yearly
|
||||
const isCurrent = plan === currentPlan
|
||||
const isPlanDisabled = planInfo.level <= ALL_PLANS[currentPlan].level
|
||||
const isCurrentPaidPlan = isCurrent && !isFreePlan
|
||||
const isPlanDisabled = isCurrentPaidPlan ? false : planInfo.level <= ALL_PLANS[currentPlan].level
|
||||
const { isCurrentWorkspaceManager } = useAppContext()
|
||||
|
||||
const btnText = useMemo(() => {
|
||||
@@ -60,10 +61,6 @@ const CloudPlanItem: FC<CloudPlanItemProps> = ({
|
||||
if (isPlanDisabled)
|
||||
return
|
||||
|
||||
if (isFreePlan)
|
||||
return
|
||||
|
||||
// Only workspace manager can buy plan
|
||||
if (!isCurrentWorkspaceManager) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
@@ -74,6 +71,15 @@ const CloudPlanItem: FC<CloudPlanItemProps> = ({
|
||||
}
|
||||
setLoading(true)
|
||||
try {
|
||||
if (isCurrentPaidPlan) {
|
||||
const res = await fetchBillingUrl()
|
||||
window.open(res.url, '_blank')
|
||||
return
|
||||
}
|
||||
|
||||
if (isFreePlan)
|
||||
return
|
||||
|
||||
const res = await fetchSubscriptionUrls(plan, isYear ? 'year' : 'month')
|
||||
// Adb Block additional tracking block the gtag, so we need to redirect directly
|
||||
window.location.href = res.url
|
||||
|
||||
@@ -25,6 +25,9 @@ const translation = {
|
||||
encourageShort: 'Upgrade',
|
||||
},
|
||||
viewBilling: 'Manage billing and subscriptions',
|
||||
viewBillingTitle: 'Billing and Subscriptions',
|
||||
viewBillingDescription: 'Manage payment methods, invoices, and subscription changes',
|
||||
viewBillingAction: 'Manage',
|
||||
buyPermissionDeniedTip: 'Please contact your enterprise administrator to subscribe',
|
||||
plansCommon: {
|
||||
title: {
|
||||
|
||||
@@ -24,6 +24,9 @@ const translation = {
|
||||
encourageShort: 'アップグレード',
|
||||
},
|
||||
viewBilling: '請求とサブスクリプションの管理',
|
||||
viewBillingTitle: '請求とサブスクリプション',
|
||||
viewBillingDescription: '支払い方法、請求書、サブスクリプションの変更の管理。',
|
||||
viewBillingAction: '管理',
|
||||
buyPermissionDeniedTip: 'サブスクリプションするには、エンタープライズ管理者に連絡してください',
|
||||
plansCommon: {
|
||||
title: {
|
||||
|
||||
@@ -24,6 +24,9 @@ const translation = {
|
||||
encourageShort: '升级',
|
||||
},
|
||||
viewBilling: '管理账单及订阅',
|
||||
viewBillingTitle: '账单与订阅',
|
||||
viewBillingDescription: '管理支付方式、发票和订阅变更。',
|
||||
viewBillingAction: '管理',
|
||||
buyPermissionDeniedTip: '请联系企业管理员订阅',
|
||||
plansCommon: {
|
||||
title: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { get } from './base'
|
||||
import { get, put } from './base'
|
||||
import type { CurrentPlanInfoBackend, SubscriptionUrlsBackend } from '@/app/components/billing/type'
|
||||
|
||||
export const fetchCurrentPlanInfo = () => {
|
||||
@@ -12,3 +12,13 @@ export const fetchSubscriptionUrls = (plan: string, interval: string) => {
|
||||
export const fetchBillingUrl = () => {
|
||||
return get<{ url: string }>('/billing/invoices')
|
||||
}
|
||||
|
||||
export const bindPartnerStackInfo = (partnerKey: string, clickId: string) => {
|
||||
return put(`/billing/partners/${partnerKey}/tenants`, {
|
||||
body: {
|
||||
click_id: clickId,
|
||||
},
|
||||
}, {
|
||||
silent: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { put } from './base'
|
||||
import { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import { bindPartnerStackInfo, fetchBillingUrl } from '@/service/billing'
|
||||
|
||||
const NAME_SPACE = 'billing'
|
||||
|
||||
export const useBindPartnerStackInfo = () => {
|
||||
return useMutation({
|
||||
mutationKey: [NAME_SPACE, 'bind-partner-stack'],
|
||||
mutationFn: (data: { partnerKey: string; clickId: string }) => {
|
||||
return put(`/billing/partners/${data.partnerKey}/tenants`, {
|
||||
body: {
|
||||
click_id: data.clickId,
|
||||
},
|
||||
}, {
|
||||
silent: true,
|
||||
})
|
||||
mutationFn: (data: { partnerKey: string; clickId: string }) => bindPartnerStackInfo(data.partnerKey, data.clickId),
|
||||
})
|
||||
}
|
||||
|
||||
export const useBillingUrl = (enabled: boolean) => {
|
||||
return useQuery({
|
||||
queryKey: [NAME_SPACE, 'url'],
|
||||
enabled,
|
||||
queryFn: async () => {
|
||||
const res = await fetchBillingUrl()
|
||||
return res.url
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user