From b8330df96e4454fd2d4a63adc6665c026d576f9e Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Fri, 7 Aug 2026 12:26:18 +0800 Subject: [PATCH] refactor(web): simplify billing portal link (#40131) --- .../billing/billing-integration.test.tsx | 18 +- .../education-verification-flow.test.tsx | 12 -- .../billing/partner-stack-flow.test.tsx | 84 ++++----- .../billing-page/__tests__/index.spec.tsx | 170 ++++++------------ .../components/billing/billing-page/index.tsx | 55 +++--- .../billing/partner-stack/use-ps-info.ts | 15 +- web/service/use-billing.ts | 24 --- 7 files changed, 130 insertions(+), 248 deletions(-) delete mode 100644 web/service/use-billing.ts diff --git a/web/__tests__/billing/billing-integration.test.tsx b/web/__tests__/billing/billing-integration.test.tsx index 088feeb2f0f..72b25cacb3b 100644 --- a/web/__tests__/billing/billing-integration.test.tsx +++ b/web/__tests__/billing/billing-integration.test.tsx @@ -34,6 +34,9 @@ const render = (ui: ReactElement, options: RenderOptions = {}, vectorSpaceUsageU limit: plan.total.vectorSpace, usage_unknown: vectorSpaceUsageUnknown, }) + queryClient.setQueryData(consoleQuery.billing.invoices.get.queryOptions().queryKey, { + url: 'https://billing.example.com', + }) const { wrapper } = createConsoleQueryWrapper({ systemFeatures: { deployment_edition: 'CLOUD' }, queryClient, @@ -71,17 +74,6 @@ vi.mock('@/context/i18n', () => ({ useGetPricingPageLanguage: () => 'en', })) -// ─── Service mocks ────────────────────────────────────────────────────────── -const mockRefetch = vi.fn().mockResolvedValue({ data: 'https://billing.example.com' }) -vi.mock('@/service/use-billing', () => ({ - useBillingUrl: () => ({ - data: 'https://billing.example.com', - isFetching: false, - refetch: mockRefetch, - }), - useBindPartnerStackInfo: () => ({ mutateAsync: vi.fn() }), -})) - vi.mock('@/service/use-education', () => ({ useEducationVerify: () => ({ mutateAsync: vi.fn().mockResolvedValue({ token: 'test-token' }), @@ -97,10 +89,6 @@ vi.mock('@/next/navigation', () => ({ useSearchParams: () => new URLSearchParams(), })) -vi.mock('@/hooks/use-async-window-open', () => ({ - useAsyncWindowOpen: () => vi.fn(), -})) - // ─── External component mocks ─────────────────────────────────────────────── vi.mock('@/app/education-apply/verify-state-modal', () => ({ default: ({ isShow }: { isShow: boolean }) => diff --git a/web/__tests__/billing/education-verification-flow.test.tsx b/web/__tests__/billing/education-verification-flow.test.tsx index e1325c21293..4bbb6547e01 100644 --- a/web/__tests__/billing/education-verification-flow.test.tsx +++ b/web/__tests__/billing/education-verification-flow.test.tsx @@ -78,14 +78,6 @@ vi.mock('@/service/use-education', () => ({ }), })) -vi.mock('@/service/use-billing', () => ({ - useBillingUrl: () => ({ - data: 'https://billing.example.com', - isFetching: false, - refetch: vi.fn(), - }), -})) - // ─── Navigation mocks ─────────────────────────────────────────────────────── vi.mock('@/next/navigation', () => ({ useRouter: () => ({ push: mockRouterPush }), @@ -93,10 +85,6 @@ vi.mock('@/next/navigation', () => ({ useSearchParams: () => new URLSearchParams(), })) -vi.mock('@/hooks/use-async-window-open', () => ({ - useAsyncWindowOpen: () => vi.fn(), -})) - // ─── External component mocks ─────────────────────────────────────────────── vi.mock('@/app/education-apply/verify-state-modal', () => ({ default: ({ diff --git a/web/__tests__/billing/partner-stack-flow.test.tsx b/web/__tests__/billing/partner-stack-flow.test.tsx index d5f89ee6859..3db886f76da 100644 --- a/web/__tests__/billing/partner-stack-flow.test.tsx +++ b/web/__tests__/billing/partner-stack-flow.test.tsx @@ -7,19 +7,26 @@ * Covers URL param reading, cookie persistence, API bind on mount, * cookie cleanup after successful bind, and error handling for 400 status. */ -import { act, cleanup, renderHook, waitFor } from '@testing-library/react' +import { act, cleanup, waitFor } from '@testing-library/react' import Cookies from 'js-cookie' import * as React from 'react' import usePSInfo from '@/app/components/billing/partner-stack/use-ps-info' import { PARTNER_STACK_CONFIG } from '@/config' -import { renderWithConsoleQuery } from '@/test/console/query-data' +import { renderHookWithConsoleQuery, renderWithConsoleQuery } from '@/test/console/query-data' const render = (ui: React.ReactElement) => renderWithConsoleQuery(ui, { systemFeatures: { deployment_edition: 'CLOUD' } }) +const renderPSInfo = () => + renderHookWithConsoleQuery(() => usePSInfo(), { + systemFeatures: { deployment_edition: 'CLOUD' }, + }) + // ─── Mock state ────────────────────────────────────────────────────────────── let mockSearchParams = new URLSearchParams() -const mockMutateAsync = vi.fn() +const mocks = vi.hoisted(() => ({ + request: vi.fn(), +})) // ─── Module mocks ──────────────────────────────────────────────────────────── vi.mock('@/next/navigation', () => ({ @@ -28,15 +35,9 @@ vi.mock('@/next/navigation', () => ({ usePathname: () => '/', })) -vi.mock('@/service/use-billing', () => ({ - useBindPartnerStackInfo: () => ({ - mutateAsync: mockMutateAsync, - }), - useBillingUrl: () => ({ - data: '', - isFetching: false, - refetch: vi.fn(), - }), +vi.mock('@/service/base', () => ({ + request: (...args: unknown[]) => mocks.request(...args), + sseGeneratorPost: vi.fn(), })) vi.mock('@/config', async (importOriginal) => { @@ -69,6 +70,8 @@ const clearCookie = () => { Cookies.remove(PARTNER_STACK_CONFIG.cookieName) } +const getBindRequest = () => mocks.request.mock.calls[0]?.[2]?.request as Request | undefined + // ═══════════════════════════════════════════════════════════════════════════════ describe('Partner Stack Flow', () => { beforeEach(() => { @@ -76,7 +79,7 @@ describe('Partner Stack Flow', () => { cleanup() clearCookie() mockSearchParams = new URLSearchParams() - mockMutateAsync.mockResolvedValue({}) + mocks.request.mockResolvedValue(Response.json({})) }) // ─── 1. URL Param Reading ─────────────────────────────────────────────── @@ -87,7 +90,7 @@ describe('Partner Stack Flow', () => { ps_xid: 'click-456', }) - const { result } = renderHook(() => usePSInfo()) + const { result } = renderPSInfo() expect(result.current.psPartnerKey).toBe('partner-123') expect(result.current.psClickId).toBe('click-456') @@ -96,7 +99,7 @@ describe('Partner Stack Flow', () => { it('should fall back to cookie when URL params are not present', () => { setCookieData({ partnerKey: 'cookie-partner', clickId: 'cookie-click' }) - const { result } = renderHook(() => usePSInfo()) + const { result } = renderPSInfo() expect(result.current.psPartnerKey).toBe('cookie-partner') expect(result.current.psClickId).toBe('cookie-click') @@ -109,14 +112,14 @@ describe('Partner Stack Flow', () => { ps_xid: 'url-click', }) - const { result } = renderHook(() => usePSInfo()) + const { result } = renderPSInfo() expect(result.current.psPartnerKey).toBe('url-partner') expect(result.current.psClickId).toBe('url-click') }) it('should return null for both values when no params and no cookie', () => { - const { result } = renderHook(() => usePSInfo()) + const { result } = renderPSInfo() expect(result.current.psPartnerKey).toBeUndefined() expect(result.current.psClickId).toBeUndefined() @@ -131,7 +134,7 @@ describe('Partner Stack Flow', () => { ps_xid: 'new-click', }) - const { result } = renderHook(() => usePSInfo()) + const { result } = renderPSInfo() act(() => result.current.saveOrUpdate()) const cookieData = getCookieData() @@ -149,7 +152,7 @@ describe('Partner Stack Flow', () => { }) const cookieSetSpy = vi.spyOn(Cookies, 'set') - const { result } = renderHook(() => usePSInfo()) + const { result } = renderPSInfo() act(() => result.current.saveOrUpdate()) // Should not call set because values haven't changed @@ -163,7 +166,7 @@ describe('Partner Stack Flow', () => { }) const cookieSetSpy = vi.spyOn(Cookies, 'set') - const { result } = renderHook(() => usePSInfo()) + const { result } = renderPSInfo() act(() => result.current.saveOrUpdate()) expect(cookieSetSpy).not.toHaveBeenCalled() @@ -176,7 +179,7 @@ describe('Partner Stack Flow', () => { }) const cookieSetSpy = vi.spyOn(Cookies, 'set') - const { result } = renderHook(() => usePSInfo()) + const { result } = renderPSInfo() act(() => result.current.saveOrUpdate()) expect(cookieSetSpy).not.toHaveBeenCalled() @@ -186,21 +189,22 @@ describe('Partner Stack Flow', () => { // ─── 3. Bind API Flow ────────────────────────────────────────────────── describe('Bind API flow', () => { - it('should call mutateAsync with partnerKey and clickId on bind', async () => { + it('should bind the partner key and click ID through the billing API', async () => { mockSearchParams = new URLSearchParams({ ps_partner_key: 'bind-partner', ps_xid: 'bind-click', }) - const { result } = renderHook(() => usePSInfo()) + const { result } = renderPSInfo() await act(async () => { await result.current.bind() }) - expect(mockMutateAsync).toHaveBeenCalledWith({ - partnerKey: 'bind-partner', - clickId: 'bind-click', - }) + expect(mocks.request).toHaveBeenCalledTimes(1) + const request = getBindRequest() + expect(request?.method).toBe('PUT') + expect(request?.url).toContain('/billing/partners/bind-partner/tenants') + await expect(request?.clone().json()).resolves.toEqual({ click_id: 'bind-click' }) }) it('should remove cookie after successful bind', async () => { @@ -210,7 +214,7 @@ describe('Partner Stack Flow', () => { ps_xid: 'rm-click', }) - const { result } = renderHook(() => usePSInfo()) + const { result } = renderPSInfo() await act(async () => { await result.current.bind() }) @@ -220,14 +224,14 @@ describe('Partner Stack Flow', () => { }) it('should remove cookie on 400 error (already bound)', async () => { - mockMutateAsync.mockRejectedValue({ status: 400 }) + mocks.request.mockRejectedValue({ status: 400 }) setCookieData({ partnerKey: 'err-partner', clickId: 'err-click' }) mockSearchParams = new URLSearchParams({ ps_partner_key: 'err-partner', ps_xid: 'err-click', }) - const { result } = renderHook(() => usePSInfo()) + const { result } = renderPSInfo() await act(async () => { await result.current.bind() }) @@ -237,14 +241,14 @@ describe('Partner Stack Flow', () => { }) it('should not remove cookie on non-400 errors', async () => { - mockMutateAsync.mockRejectedValue({ status: 500 }) + mocks.request.mockRejectedValue({ status: 500 }) setCookieData({ partnerKey: 'keep-partner', clickId: 'keep-click' }) mockSearchParams = new URLSearchParams({ ps_partner_key: 'keep-partner', ps_xid: 'keep-click', }) - const { result } = renderHook(() => usePSInfo()) + const { result } = renderPSInfo() await act(async () => { await result.current.bind() }) @@ -259,12 +263,12 @@ describe('Partner Stack Flow', () => { ps_xid: 'click-only', }) - const { result } = renderHook(() => usePSInfo()) + const { result } = renderPSInfo() await act(async () => { await result.current.bind() }) - expect(mockMutateAsync).not.toHaveBeenCalled() + expect(mocks.request).not.toHaveBeenCalled() }) it('should not call bind a second time (idempotency)', async () => { @@ -273,19 +277,19 @@ describe('Partner Stack Flow', () => { ps_xid: 'click-once', }) - const { result } = renderHook(() => usePSInfo()) + const { result } = renderPSInfo() // First bind await act(async () => { await result.current.bind() }) - expect(mockMutateAsync).toHaveBeenCalledTimes(1) + expect(mocks.request).toHaveBeenCalledTimes(1) // Second bind should be skipped (hasBind = true) await act(async () => { await result.current.bind() }) - expect(mockMutateAsync).toHaveBeenCalledTimes(1) + expect(mocks.request).toHaveBeenCalledTimes(1) }) }) @@ -303,10 +307,8 @@ describe('Partner Stack Flow', () => { render() await waitFor(() => { - expect(mockMutateAsync).toHaveBeenCalledWith({ - partnerKey: 'mount-partner', - clickId: 'mount-click', - }) + expect(mocks.request).toHaveBeenCalledTimes(1) + expect(getBindRequest()?.url).toContain('/billing/partners/mount-partner/tenants') expect(Cookies.get(PARTNER_STACK_CONFIG.cookieName)).toBeUndefined() }) }) diff --git a/web/app/components/billing/billing-page/__tests__/index.spec.tsx b/web/app/components/billing/billing-page/__tests__/index.spec.tsx index bb30271955e..3a283d52435 100644 --- a/web/app/components/billing/billing-page/__tests__/index.spec.tsx +++ b/web/app/components/billing/billing-page/__tests__/index.spec.tsx @@ -1,37 +1,21 @@ -import { fireEvent, screen, waitFor } from '@testing-library/react' +import { screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { consoleQuery } from '@/service/client' +import { createConsoleQueryClient, createConsoleQueryWrapper } from '@/test/console/query-data' import { render } from '@/test/console/render' import Billing from '../index' -let currentBillingUrl: string | null = 'https://billing' -let fetching = false +let currentBillingUrl: string | undefined = 'https://billing.example.com' let isManager = true let enableBilling = true -let billingUrlEnabled = false -const refetchMock = vi.fn() -const openAsyncWindowMock = vi.fn() - -type BillingUrlCallback = () => Promise -type BillingWindowOptions = { - immediateUrl: string | null - features: string - onError: (err: Error) => void -} -type OpenAsyncWindowCall = [BillingUrlCallback, BillingWindowOptions] - -vi.mock('@/service/use-billing', () => ({ - useBillingUrl: (enabled: boolean) => { - billingUrlEnabled = enabled - return { - data: currentBillingUrl, - isFetching: fetching, - refetch: refetchMock, - } - }, +const mocks = vi.hoisted(() => ({ + request: vi.fn(() => new Promise(() => {})), })) -vi.mock('@/hooks/use-async-window-open', () => ({ - useAsyncWindowOpen: () => openAsyncWindowMock, +vi.mock('@/service/base', () => ({ + request: mocks.request, + sseGeneratorPost: vi.fn(), })) vi.mock('@/context/workspace-state', async () => { @@ -51,113 +35,67 @@ vi.mock('../../plan', () => ({ default: ({ loc }: { loc: string }) => , })) +const renderBilling = () => { + const queryClient = createConsoleQueryClient() + if (currentBillingUrl) { + queryClient.setQueryData(consoleQuery.billing.invoices.get.queryOptions().queryKey, { + url: currentBillingUrl, + }) + } + const { wrapper } = createConsoleQueryWrapper({ queryClient }) + + return render(, { wrapper }) +} + describe('Billing', () => { beforeEach(() => { vi.clearAllMocks() - currentBillingUrl = 'https://billing' - fetching = false + currentBillingUrl = 'https://billing.example.com' isManager = true enableBilling = true - billingUrlEnabled = false - refetchMock.mockResolvedValue({ data: 'https://billing' }) + }) + + it('renders the billing portal as a keyboard-accessible external link for workspace managers', async () => { + const user = userEvent.setup() + renderBilling() + + const billingLink = screen.getByRole('link', { name: /billing\.viewBillingTitle/ }) + expect(billingLink).toHaveAttribute('href', currentBillingUrl) + expect(billingLink).toHaveAttribute('target', '_blank') + expect(billingLink).toHaveAttribute('rel', 'noopener noreferrer') + + await user.tab() + expect(billingLink).toHaveFocus() }) it('hides the billing action from non-manager members', () => { isManager = false - render() + renderBilling() - expect( - screen.queryByRole('button', { name: /billing\.viewBillingTitle/ }), - ).not.toBeInTheDocument() - expect(billingUrlEnabled).toBe(false) - }) - - it('shows the billing action to managers without billing permission keys', () => { - render() - - expect(screen.getByRole('button', { name: /billing\.viewBillingTitle/ })).toBeInTheDocument() - expect(billingUrlEnabled).toBe(true) + expect(screen.queryByText('billing.viewBillingTitle')).not.toBeInTheDocument() }) it('hides the billing action when billing is disabled', () => { enableBilling = false - render() + + renderBilling() + + expect(screen.queryByText('billing.viewBillingTitle')).not.toBeInTheDocument() + }) + + it('renders the billing action outside the tab order before the URL is available', async () => { + const user = userEvent.setup() + currentBillingUrl = undefined + + renderBilling() + + expect(screen.getByText('billing.viewBillingTitle')).toBeInTheDocument() expect( - screen.queryByRole('button', { name: /billing\.viewBillingTitle/ }), + screen.queryByRole('link', { name: /billing\.viewBillingTitle/ }), ).not.toBeInTheDocument() - expect(billingUrlEnabled).toBe(false) - }) - it('opens the billing window with the immediate url when the button is clicked', async () => { - render() - - const actionButton = screen.getByRole('button', { name: /billing\.viewBillingTitle/ }) - fireEvent.click(actionButton) - - await waitFor(() => expect(openAsyncWindowMock).toHaveBeenCalled()) - const [, options] = openAsyncWindowMock.mock.calls[0] as OpenAsyncWindowCall - expect(options).toMatchObject({ - immediateUrl: currentBillingUrl, - features: 'noopener,noreferrer', - }) - }) - - it('returns the refetched url from the async callback', async () => { - const newUrl = 'https://new-billing-url' - refetchMock.mockResolvedValue({ data: newUrl }) - render() - - const actionButton = screen.getByRole('button', { name: /billing\.viewBillingTitle/ }) - fireEvent.click(actionButton) - - await waitFor(() => expect(openAsyncWindowMock).toHaveBeenCalled()) - const [asyncCallback] = openAsyncWindowMock.mock.calls[0] as OpenAsyncWindowCall - - // Execute the async callback passed to openAsyncWindow - const result = await asyncCallback() - expect(result).toBe(newUrl) - expect(refetchMock).toHaveBeenCalled() - }) - - it('returns null when refetch returns no url', async () => { - refetchMock.mockResolvedValue({ data: null }) - render() - - const actionButton = screen.getByRole('button', { name: /billing\.viewBillingTitle/ }) - fireEvent.click(actionButton) - - await waitFor(() => expect(openAsyncWindowMock).toHaveBeenCalled()) - const [asyncCallback] = openAsyncWindowMock.mock.calls[0] as OpenAsyncWindowCall - - // Execute the async callback when url is null - const result = await asyncCallback() - expect(result).toBeNull() - }) - - it('handles errors in onError callback', async () => { - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) - render() - - const actionButton = screen.getByRole('button', { name: /billing\.viewBillingTitle/ }) - fireEvent.click(actionButton) - - await waitFor(() => expect(openAsyncWindowMock).toHaveBeenCalled()) - const [, options] = openAsyncWindowMock.mock.calls[0] as OpenAsyncWindowCall - - // Execute the onError callback - const testError = new Error('Test error') - options.onError(testError) - expect(consoleError).toHaveBeenCalledWith('Failed to fetch billing url', testError) - - consoleError.mockRestore() - }) - - it('disables the button while billing url is fetching', () => { - fetching = true - render() - - const actionButton = screen.getByRole('button', { name: /billing\.viewBillingTitle/ }) - expect(actionButton)!.toBeDisabled() + await user.tab() + expect(document.body).toHaveFocus() }) }) diff --git a/web/app/components/billing/billing-page/index.tsx b/web/app/components/billing/billing-page/index.tsx index e9877b21404..7b27f632412 100644 --- a/web/app/components/billing/billing-page/index.tsx +++ b/web/app/components/billing/billing-page/index.tsx @@ -1,51 +1,38 @@ 'use client' import type { FC } from 'react' +import { cn } from '@langgenius/dify-ui/cn' +import { useQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' import * as React from 'react' import { useTranslation } from 'react-i18next' import { useProviderContext } from '@/context/provider-context' import { isCurrentWorkspaceManagerAtom } from '@/context/workspace-state' -import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' -import { useBillingUrl } from '@/service/use-billing' +import { consoleQuery } from '@/service/client' import PlanComp from '../plan' const Billing: FC = () => { const { t } = useTranslation() const isCurrentWorkspaceManager = useAtomValue(isCurrentWorkspaceManagerAtom) const { enableBilling } = useProviderContext() - const { - data: billingUrl, - isFetching, - refetch, - } = useBillingUrl(enableBilling && isCurrentWorkspaceManager) - const openAsyncWindow = useAsyncWindowOpen() - - const handleOpenBilling = async () => { - await openAsyncWindow( - async () => { - const url = (await refetch()).data - if (url) return url - return null - }, - { - immediateUrl: billingUrl, - features: 'noopener,noreferrer', - onError: (err) => { - console.error('Failed to fetch billing url', err) - }, - }, - ) - } + const canManageBilling = enableBilling && isCurrentWorkspaceManager + const { data: billing } = useQuery( + consoleQuery.billing.invoices.get.queryOptions({ enabled: canManageBilling }), + ) + const billingUrl = billing?.url return ( - {enableBilling && isCurrentWorkspaceManager && ( - @@ -55,13 +42,13 @@ const Billing: FC = () => { {t(($) => $.viewBillingDescription, { ns: 'billing' })} - + {t(($) => $.viewBillingAction, { ns: 'billing' })} - + - + )} ) diff --git a/web/app/components/billing/partner-stack/use-ps-info.ts b/web/app/components/billing/partner-stack/use-ps-info.ts index 63efad4a8f9..7fdeb260f21 100644 --- a/web/app/components/billing/partner-stack/use-ps-info.ts +++ b/web/app/components/billing/partner-stack/use-ps-info.ts @@ -1,9 +1,10 @@ +import { useMutation } from '@tanstack/react-query' import { useBoolean } from 'ahooks' import Cookies from 'js-cookie' import { useCallback } from 'react' import { PARTNER_STACK_CONFIG } from '@/config' import { useSearchParams } from '@/next/navigation' -import { useBindPartnerStackInfo } from '@/service/use-billing' +import { consoleQuery } from '@/service/client' const usePSInfo = () => { const searchParams = useSearchParams() @@ -20,7 +21,9 @@ const usePSInfo = () => { const isPSChanged = psInfoInCookie?.partnerKey !== psPartnerKey || psInfoInCookie?.clickId !== psClickId const [hasBind, { setTrue: setBind }] = useBoolean(false) - const { mutateAsync } = useBindPartnerStackInfo() + const { mutateAsync: bindPartnerStackInfo } = useMutation( + consoleQuery.billing.partners.byPartnerKey.tenants.put.mutationOptions(), + ) // Save to top domain. cloud.dify.ai => .dify.ai const domain = globalThis.location?.hostname.replace('cloud', '') @@ -45,9 +48,9 @@ const usePSInfo = () => { if (psPartnerKey && psClickId && !hasBind) { let shouldRemoveCookie = false try { - await mutateAsync({ - partnerKey: psPartnerKey, - clickId: psClickId, + await bindPartnerStackInfo({ + params: { partner_key: psPartnerKey }, + body: { click_id: psClickId }, }) shouldRemoveCookie = true } catch (error: unknown) { @@ -56,7 +59,7 @@ const usePSInfo = () => { if (shouldRemoveCookie) Cookies.remove(PARTNER_STACK_CONFIG.cookieName, { path: '/', domain }) setBind() } - }, [psPartnerKey, psClickId, hasBind, domain, setBind, mutateAsync]) + }, [psPartnerKey, psClickId, hasBind, domain, setBind, bindPartnerStackInfo]) return { psPartnerKey, psClickId, diff --git a/web/service/use-billing.ts b/web/service/use-billing.ts deleted file mode 100644 index 5cf11b3e0ae..00000000000 --- a/web/service/use-billing.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { useMutation, useQuery } from '@tanstack/react-query' -import { consoleClient, consoleQuery } from '@/service/client' - -export const useBindPartnerStackInfo = () => { - return useMutation({ - mutationKey: consoleQuery.billing.partners.byPartnerKey.tenants.put.mutationKey(), - mutationFn: (data: { partnerKey: string; clickId: string }) => - consoleClient.billing.partners.byPartnerKey.tenants.put({ - params: { partner_key: data.partnerKey }, - body: { click_id: data.clickId }, - }), - }) -} - -export const useBillingUrl = (enabled: boolean) => { - return useQuery({ - queryKey: consoleQuery.billing.invoices.get.queryKey(), - enabled, - queryFn: async () => { - const res = await consoleClient.billing.invoices.get() - return res.url - }, - }) -}