diff --git a/web/app/components/billing/billing-page/index.tsx b/web/app/components/billing/billing-page/index.tsx
index 43e80f4bc4..adb676cde1 100644
--- a/web/app/components/billing/billing-page/index.tsx
+++ b/web/app/components/billing/billing-page/index.tsx
@@ -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 (
)
diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx
index 5d7b11c7e0..396dd4a1b0 100644
--- a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx
+++ b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx
@@ -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 = ({
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 = ({
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 = ({
}
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
diff --git a/web/i18n/en-US/billing.ts b/web/i18n/en-US/billing.ts
index 7272214f41..2531e5831a 100644
--- a/web/i18n/en-US/billing.ts
+++ b/web/i18n/en-US/billing.ts
@@ -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: {
diff --git a/web/i18n/ja-JP/billing.ts b/web/i18n/ja-JP/billing.ts
index 57ccef5491..561b37a825 100644
--- a/web/i18n/ja-JP/billing.ts
+++ b/web/i18n/ja-JP/billing.ts
@@ -24,6 +24,9 @@ const translation = {
encourageShort: 'アップグレード',
},
viewBilling: '請求とサブスクリプションの管理',
+ viewBillingTitle: '請求とサブスクリプション',
+ viewBillingDescription: '支払い方法、請求書、サブスクリプションの変更の管理。',
+ viewBillingAction: '管理',
buyPermissionDeniedTip: 'サブスクリプションするには、エンタープライズ管理者に連絡してください',
plansCommon: {
title: {
diff --git a/web/i18n/zh-Hans/billing.ts b/web/i18n/zh-Hans/billing.ts
index ee8f7af64d..a6237bef2e 100644
--- a/web/i18n/zh-Hans/billing.ts
+++ b/web/i18n/zh-Hans/billing.ts
@@ -24,6 +24,9 @@ const translation = {
encourageShort: '升级',
},
viewBilling: '管理账单及订阅',
+ viewBillingTitle: '账单与订阅',
+ viewBillingDescription: '管理支付方式、发票和订阅变更。',
+ viewBillingAction: '管理',
buyPermissionDeniedTip: '请联系企业管理员订阅',
plansCommon: {
title: {
diff --git a/web/service/billing.ts b/web/service/billing.ts
index 7dfe5ac0a7..979a888582 100644
--- a/web/service/billing.ts
+++ b/web/service/billing.ts
@@ -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,
+ })
+}
diff --git a/web/service/use-billing.ts b/web/service/use-billing.ts
index b48a75eab0..2701861bc0 100644
--- a/web/service/use-billing.ts
+++ b/web/service/use-billing.ts
@@ -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
},
})
}