refactor(web): migrate billing app context consumers (#38541)

This commit is contained in:
Stephen Zhou 2026-07-08 15:02:47 +08:00 committed by GitHub
parent 3523da508f
commit 5a210cf03e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 230 additions and 44 deletions

View File

@ -36,6 +36,16 @@ vi.mock('@/context/app-context', () => ({
useAppContext: () => mockAppCtx,
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => mockAppCtx)
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/context/modal-context', () => ({
useModalContext: () => ({
setShowPricingModal: mockSetShowPricingModal,

View File

@ -28,6 +28,16 @@ vi.mock('@/context/app-context', () => ({
useAppContext: () => mockAppCtx,
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => mockAppCtx)
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/context/i18n', () => ({
useGetLanguage: () => 'en-US',
}))

View File

@ -41,6 +41,16 @@ vi.mock('@/context/app-context', () => ({
useAppContext: () => mockAppCtx,
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => mockAppCtx)
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/context/modal-context', () => ({
useModalContext: () => ({
setShowPricingModal: mockSetShowPricingModal,

View File

@ -28,6 +28,16 @@ vi.mock('@/context/app-context', () => ({
useAppContext: () => mockAppCtx,
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => mockAppCtx)
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/context/i18n', () => ({
useGetLanguage: () => 'en-US',
useGetPricingPageLanguage: () => 'en',

View File

@ -24,6 +24,16 @@ vi.mock('@/context/app-context', () => ({
useAppContext: () => mockAppCtx,
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => mockAppCtx)
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/context/i18n', () => ({
useGetLanguage: () => 'en-US',
}))

View File

@ -7,8 +7,8 @@ export type AppContextStateMockState = {
id?: string
name?: string
email?: string
avatar?: string
avatar_url?: string
avatar?: string | null
avatar_url?: string | null
is_password_set?: boolean
} | null
currentWorkspace?: {
@ -27,13 +27,16 @@ export type AppContextStateMockState = {
type AppContextStateAtomKind
= | 'userProfile'
| 'userProfileId'
| 'userProfileEmail'
| 'currentWorkspace'
| 'currentWorkspaceId'
| 'workspaceRoleFlags'
| 'isCurrentWorkspaceManager'
| 'currentWorkspaceLoading'
| 'workspacePermissionKeys'
| 'workspacePermissionKeysLoading'
| 'langGeniusVersionInfo'
| 'langGeniusCurrentVersion'
type AppContextStateMockAtom = {
[APP_CONTEXT_STATE_ATOM_KIND]: AppContextStateAtomKind
@ -101,13 +104,16 @@ export const createAppContextStateAtomMock = async (
...actual,
userProfileAtom: createMockAtom('userProfile'),
userProfileIdAtom: createMockAtom('userProfileId'),
userProfileEmailAtom: createMockAtom('userProfileEmail'),
currentWorkspaceAtom: createMockAtom('currentWorkspace'),
currentWorkspaceIdAtom: createMockAtom('currentWorkspaceId'),
workspaceRoleFlagsAtom: createMockAtom('workspaceRoleFlags'),
isCurrentWorkspaceManagerAtom: createMockAtom('isCurrentWorkspaceManager'),
currentWorkspaceLoadingAtom: createMockAtom('currentWorkspaceLoading'),
workspacePermissionKeysAtom: createMockAtom('workspacePermissionKeys'),
workspacePermissionKeysLoadingAtom: createMockAtom('workspacePermissionKeysLoading'),
langGeniusVersionInfoAtom: createMockAtom('langGeniusVersionInfo'),
langGeniusCurrentVersionAtom: createMockAtom('langGeniusCurrentVersion'),
}
}
@ -135,6 +141,9 @@ export const createAppContextStateJotaiMock = async (
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'userProfileId')
return userProfile.id
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'userProfileEmail')
return userProfile.email
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'currentWorkspace')
return currentWorkspace
@ -150,6 +159,9 @@ export const createAppContextStateJotaiMock = async (
}
}
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'isCurrentWorkspaceManager')
return state.isCurrentWorkspaceManager ?? false
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'currentWorkspaceLoading')
return state.isLoadingCurrentWorkspace ?? false
@ -162,6 +174,9 @@ export const createAppContextStateJotaiMock = async (
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'langGeniusVersionInfo')
return state.langGeniusVersionInfo ?? defaultLangGeniusVersionInfo
if (atom[APP_CONTEXT_STATE_ATOM_KIND] === 'langGeniusCurrentVersion')
return (state.langGeniusVersionInfo ?? defaultLangGeniusVersionInfo).current_version
throw new Error(`Unsupported app context state atom: ${atom[APP_CONTEXT_STATE_ATOM_KIND]}`)
},
}

View File

@ -11,6 +11,8 @@ import { useAppContext } from '@/context/app-context'
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
import AppsFull from '../index'
let mockAppContextState: AppContextValue
vi.mock('@/config', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/config')>()
return {
@ -23,6 +25,16 @@ vi.mock('@/context/app-context', () => ({
useAppContext: vi.fn(),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => mockAppContextState)
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/context/provider-context', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/context/provider-context')>()
return {
@ -123,7 +135,8 @@ describe('AppsFull', () => {
beforeEach(() => {
vi.clearAllMocks()
;(useProviderContext as Mock).mockReturnValue(buildProviderContext())
;(useAppContext as Mock).mockReturnValue(buildAppContext())
mockAppContextState = buildAppContext()
;(useAppContext as Mock).mockReturnValue(mockAppContextState)
;(mailToSupport as Mock).mockReturnValue('mailto:support@example.com')
})

View File

@ -4,11 +4,12 @@ import type { FC } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { MeterIndicator, MeterRoot, MeterTrack } from '@langgenius/dify-ui/meter'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { Plan } from '@/app/components/billing/type'
import { mailToSupport } from '@/app/components/header/utils/util'
import { useAppContext } from '@/context/app-context'
import { langGeniusCurrentVersionAtom, userProfileEmailAtom } from '@/context/app-context-state'
import { useProviderContext } from '@/context/provider-context'
import UpgradeBtn from '../upgrade-btn'
import s from './style.module.css'
@ -19,7 +20,8 @@ const AppsFull: FC<{ loc: string, className?: string }> = ({
}) => {
const { t } = useTranslation()
const { plan } = useProviderContext()
const { userProfile, langGeniusVersionInfo } = useAppContext()
const userProfileEmail = useAtomValue(userProfileEmailAtom)
const currentVersion = useAtomValue(langGeniusCurrentVersionAtom)
const isTeam = plan.type === Plan.team
const usage = plan.usage.buildApps
const total = plan.total.buildApps
@ -54,7 +56,7 @@ const AppsFull: FC<{ loc: string, className?: string }> = ({
)}
{plan.type !== Plan.sandbox && plan.type !== Plan.professional && (
<Button variant="secondary-accent">
<a target="_blank" rel="noopener noreferrer" href={mailToSupport(userProfile.email, plan.type, langGeniusVersionInfo.current_version)}>
<a target="_blank" rel="noopener noreferrer" href={mailToSupport(userProfileEmail, plan.type, currentVersion)}>
{t('apps.contactUs', { ns: 'billing' })}
</a>
</Button>

View File

@ -41,6 +41,19 @@ vi.mock('@/context/app-context', () => ({
}),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => ({
isCurrentWorkspaceManager: isManager,
workspacePermissionKeys,
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/context/provider-context', () => ({
useProviderContext: () => ({
enableBilling,

View File

@ -1,8 +1,9 @@
'use client'
import type { FC } from 'react'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { useAppContext } from '@/context/app-context'
import { isCurrentWorkspaceManagerAtom, workspacePermissionKeysAtom } from '@/context/app-context-state'
import { useProviderContext } from '@/context/provider-context'
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
import { useBillingUrl } from '@/service/use-billing'
@ -11,7 +12,8 @@ import PlanComp from '../plan'
const Billing: FC = () => {
const { t } = useTranslation()
const { isCurrentWorkspaceManager, workspacePermissionKeys } = useAppContext()
const isCurrentWorkspaceManager = useAtomValue(isCurrentWorkspaceManagerAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const { enableBilling } = useProviderContext()
const canManageBillingSubscription = isCurrentWorkspaceManager && hasPermission(workspacePermissionKeys, BillingPermission.SubscriptionManage)
const { data: billingUrl, isFetching, refetch } = useBillingUrl(enableBilling && canManageBillingSubscription)

View File

@ -1,15 +1,16 @@
'use client'
import { toast } from '@langgenius/dify-ui/toast'
import { useAtomValue } from 'jotai'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useAppContext } from '@/context/app-context'
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
import { fetchSubscriptionUrls } from '@/service/billing'
import { BillingPermission, hasPermission } from '@/utils/permission'
import { Plan } from '../type'
export const useEducationDiscount = () => {
const { t } = useTranslation()
const { workspacePermissionKeys } = useAppContext()
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const [isEducationDiscountLoading, setIsEducationDiscountLoading] = useState(false)
const canManageBilling = hasPermission(workspacePermissionKeys, BillingPermission.Manage)

View File

@ -21,11 +21,16 @@ vi.mock('@/next/navigation', () => ({
usePathname: () => currentPath,
}))
vi.mock('@/config', () => ({
get IS_CLOUD_EDITION() {
return mockConfig.isCloudEdition
},
}))
vi.mock('@/config', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/config')>()
return {
...actual,
get IS_CLOUD_EDITION() {
return mockConfig.isCloudEdition
},
}
})
const setShowAccountSettingModalMock = vi.fn()
vi.mock('@/context/modal-context', () => ({
@ -47,6 +52,20 @@ vi.mock('@/context/app-context', () => ({
}),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => ({
userProfile: { email: 'user@example.com' },
isCurrentWorkspaceManager,
workspacePermissionKeys,
}))
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/app/education-apply/storage', () => ({
useSetEducationVerifying: () => setEducationVerifyingMock,
}))

View File

@ -7,6 +7,7 @@ import {
RiGroupLine,
} from '@remixicon/react'
import { useUnmountedRef } from 'ahooks'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
@ -15,7 +16,7 @@ import UsageInfo from '@/app/components/billing/usage-info'
import { useSetEducationVerifying } from '@/app/education-apply/storage'
import VerifyStateModal from '@/app/education-apply/verify-state-modal'
import { IS_CLOUD_EDITION } from '@/config'
import { useAppContext } from '@/context/app-context'
import { userProfileEmailAtom, workspacePermissionKeysAtom } from '@/context/app-context-state'
import { useModalContextSelector } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { usePathname, useRouter } from '@/next/navigation'
@ -41,7 +42,8 @@ const PlanComp: FC<Props> = ({
const { t } = useTranslation()
const router = useRouter()
const path = usePathname()
const { userProfile, workspacePermissionKeys } = useAppContext()
const userProfileEmail = useAtomValue(userProfileEmailAtom)
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const { plan, enableEducationPlan, allowRefreshEducationVerify, isEducationAccount } = useProviderContext()
const isAboutToExpire = allowRefreshEducationVerify
const {
@ -181,7 +183,7 @@ const PlanComp: FC<Props> = ({
</div>
<VerifyStateModal
showLink
email={userProfile.email}
email={userProfileEmail}
isShow={showModal}
title={t('rejectTitle', { ns: 'education' })}
content={t('rejectContent', { ns: 'education' })}

View File

@ -2,7 +2,6 @@ import type { ReactNode } from 'react'
import type { Mock } from 'vitest'
import type { UsagePlanInfo } from '../../type'
import { render } from '@testing-library/react'
import { useAppContext } from '@/context/app-context'
import { useGetPricingPageLanguage } from '@/context/i18n'
import { useProviderContext } from '@/context/provider-context'
import { Plan } from '../../type'
@ -15,6 +14,7 @@ type DialogProps = {
}
let latestOnOpenChange: DialogProps['onOpenChange']
let mockAppCtx: Record<string, unknown> = {}
vi.mock('@langgenius/dify-ui/dialog', () => ({
Dialog: ({ children, onOpenChange }: DialogProps) => {
@ -45,9 +45,19 @@ vi.mock('../footer', () => ({
}))
vi.mock('@/context/app-context', () => ({
useAppContext: vi.fn(),
useAppContext: () => mockAppCtx,
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => mockAppCtx)
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/context/provider-context', () => ({
useProviderContext: vi.fn(),
}))
@ -70,10 +80,10 @@ describe('Pricing dialog lifecycle', () => {
beforeEach(() => {
vi.clearAllMocks()
latestOnOpenChange = undefined
;(useAppContext as Mock).mockReturnValue({
mockAppCtx = {
isCurrentWorkspaceManager: true,
workspacePermissionKeys: ['billing.manage'],
})
}
;(useProviderContext as Mock).mockReturnValue({
plan: {
type: Plan.sandbox,

View File

@ -2,13 +2,13 @@ import type { Mock } from 'vitest'
import type { UsagePlanInfo } from '../../type'
import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import { useAppContext } from '@/context/app-context'
import { useGetPricingPageLanguage } from '@/context/i18n'
import { useProviderContext } from '@/context/provider-context'
import { Plan } from '../../type'
import Pricing from '../index'
let mockLanguage: string | null = 'en'
let mockAppCtx: Record<string, unknown> = {}
vi.mock('../plans/self-hosted-plan-item/list', () => ({
default: ({ plan }: { plan: string }) => (
@ -28,9 +28,19 @@ vi.mock('@/next/link', () => ({
}))
vi.mock('@/context/app-context', () => ({
useAppContext: vi.fn(),
useAppContext: () => mockAppCtx,
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => mockAppCtx)
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/context/provider-context', () => ({
useProviderContext: vi.fn(),
}))
@ -53,10 +63,10 @@ describe('Pricing', () => {
beforeEach(() => {
vi.clearAllMocks()
mockLanguage = 'en'
;(useAppContext as Mock).mockReturnValue({
mockAppCtx = {
isCurrentWorkspaceManager: true,
workspacePermissionKeys: ['billing.manage'],
})
}
;(useProviderContext as Mock).mockReturnValue({
plan: {
type: Plan.sandbox,
@ -79,10 +89,10 @@ describe('Pricing', () => {
})
it('should default to yearly billing for education accounts', () => {
;(useAppContext as Mock).mockReturnValue({
mockAppCtx = {
isCurrentWorkspaceManager: false,
workspacePermissionKeys: ['billing.manage'],
})
}
;(useProviderContext as Mock).mockReturnValue({
plan: {
type: Plan.sandbox,
@ -99,10 +109,10 @@ describe('Pricing', () => {
})
it('should not default to yearly billing when billing manage permission is missing', () => {
;(useAppContext as Mock).mockReturnValue({
mockAppCtx = {
isCurrentWorkspaceManager: true,
workspacePermissionKeys: [],
})
}
;(useProviderContext as Mock).mockReturnValue({
plan: {
type: Plan.sandbox,

View File

@ -10,9 +10,10 @@ import {
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useState } from 'react'
import { useAppContext } from '@/context/app-context'
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
import { useGetPricingPageLanguage } from '@/context/i18n'
import { useProviderContext } from '@/context/provider-context'
import { BillingPermission, hasPermission } from '@/utils/permission'
@ -32,7 +33,7 @@ const Pricing: FC<PricingProps> = ({
onCancel,
}) => {
const { plan, enableEducationPlan, isEducationAccount } = useProviderContext()
const { workspacePermissionKeys } = useAppContext()
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const canManageBilling = hasPermission(workspacePermissionKeys, BillingPermission.Manage)
const shouldDefaultToYearly = canManageBilling && enableEducationPlan && isEducationAccount
const [selectedPlanRange, setSelectedPlanRange] = React.useState<PlanRange>()

View File

@ -12,10 +12,22 @@ import { Plan } from '../../../../type'
import { PlanRange } from '../../../plan-switcher/plan-range-switcher'
import CloudPlanItem from '../index'
let mockAppCtx: Record<string, unknown> = {}
vi.mock('@/context/app-context', () => ({
useAppContext: vi.fn(),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => mockAppCtx)
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('@/context/provider-context', () => ({
useProviderContext: vi.fn(),
}))
@ -53,6 +65,11 @@ const mockFetchSubscriptionUrls = fetchSubscriptionUrls as Mock
let assignedHref = ''
const originalLocation = window.location
const mockAppContext = (state: Record<string, unknown>) => {
mockAppCtx = state
mockUseAppContext.mockReturnValue(state)
}
const renderWithToastHost = (ui: React.ReactNode) => {
return render(
<>
@ -79,7 +96,7 @@ beforeAll(() => {
beforeEach(() => {
vi.clearAllMocks()
toast.dismiss()
mockUseAppContext.mockReturnValue({
mockAppContext({
isCurrentWorkspaceManager: true,
workspacePermissionKeys: [
'billing.view',
@ -183,7 +200,7 @@ describe('CloudPlanItem', () => {
// Payment actions triggered from the CTA
describe('Plan purchase flow', () => {
it('should show toast when billing manage permission is missing for plan purchase', () => {
mockUseAppContext.mockReturnValue({
mockAppContext({
isCurrentWorkspaceManager: true,
workspacePermissionKeys: ['billing.subscription.manage'],
})
@ -206,7 +223,7 @@ describe('CloudPlanItem', () => {
it('should open billing portal when upgrading current paid plan', async () => {
const openWindow = vi.fn(async (cb: () => Promise<string>) => await cb())
mockUseAsyncWindowOpen.mockReturnValue(openWindow)
mockUseAppContext.mockReturnValue({
mockAppContext({
isCurrentWorkspaceManager: false,
workspacePermissionKeys: ['billing.subscription.manage'],
})
@ -229,7 +246,7 @@ describe('CloudPlanItem', () => {
})
it('should redirect to subscription url when selecting a new paid plan', async () => {
mockUseAppContext.mockReturnValue({
mockAppContext({
isCurrentWorkspaceManager: false,
workspacePermissionKeys: ['billing.manage'],
})
@ -316,7 +333,7 @@ describe('CloudPlanItem', () => {
})
it('should show default CTA and hide warning when billing manage permission is missing', () => {
mockUseAppContext.mockReturnValue({
mockAppContext({
isCurrentWorkspaceManager: true,
workspacePermissionKeys: [],
})
@ -340,7 +357,7 @@ describe('CloudPlanItem', () => {
})
it('should hide education unsupported warning when billing manage permission is missing', () => {
mockUseAppContext.mockReturnValue({
mockAppContext({
isCurrentWorkspaceManager: true,
workspacePermissionKeys: [],
})

View File

@ -10,10 +10,11 @@ import {
DialogTitle,
} from '@langgenius/dify-ui/dialog'
import { toast } from '@langgenius/dify-ui/toast'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useAppContext } from '@/context/app-context'
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
import { useProviderContext } from '@/context/provider-context'
import { useAsyncWindowOpen } from '@/hooks/use-async-window-open'
import { fetchSubscriptionUrls } from '@/service/billing'
@ -56,7 +57,7 @@ const CloudPlanItem: FC<CloudPlanItemProps> = ({
const isCurrent = plan === currentPlan
const isCurrentPaidPlan = isCurrent && !isFreePlan
const isPlanDisabled = isCurrentPaidPlan ? false : planInfo.level <= ALL_PLANS[currentPlan].level
const { workspacePermissionKeys } = useAppContext()
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const canManageBilling = hasPermission(workspacePermissionKeys, BillingPermission.Manage)
const canManageBillingSubscription = hasPermission(workspacePermissionKeys, BillingPermission.SubscriptionManage)
const { enableEducationPlan, isEducationAccount } = useProviderContext()

View File

@ -7,6 +7,8 @@ import { contactSalesUrl, getStartedWithCommunityUrl, getWithPremiumUrl } from '
import { SelfHostedPlan } from '../../../../type'
import SelfHostedPlanItem from '../index'
let mockAppCtx: Record<string, unknown> = {}
vi.mock('../list', () => ({
default: ({ plan }: { plan: string }) => (
<div data-testid={`list-${plan}`}>
@ -20,6 +22,16 @@ vi.mock('@/context/app-context', () => ({
useAppContext: vi.fn(),
}))
vi.mock('@/context/app-context-state', async (importOriginal) => {
const { createAppContextStateAtomMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateAtomMock(importOriginal, () => mockAppCtx)
})
vi.mock('jotai', async (importOriginal) => {
const { createAppContextStateJotaiMock } = await import('@/__tests__/utils/mock-app-context-state')
return createAppContextStateJotaiMock(importOriginal)
})
vi.mock('../../../assets', () => ({
Community: () => <div>Community Icon</div>,
Premium: () => <div>Premium Icon</div>,
@ -33,6 +45,11 @@ const mockUseAppContext = useAppContext as Mock
let assignedHref = ''
const originalLocation = window.location
const mockAppContext = (state: Record<string, unknown>) => {
mockAppCtx = state
mockUseAppContext.mockReturnValue(state)
}
const renderWithToastHost = (ui: React.ReactNode) => {
return render(
<>
@ -59,7 +76,7 @@ beforeAll(() => {
beforeEach(() => {
vi.clearAllMocks()
toast.dismiss()
mockUseAppContext.mockReturnValue({
mockAppContext({
isCurrentWorkspaceManager: false,
workspacePermissionKeys: ['billing.manage'],
})
@ -94,7 +111,7 @@ describe('SelfHostedPlanItem', () => {
describe('CTA interactions', () => {
it('should show toast when billing manage permission is missing', () => {
mockUseAppContext.mockReturnValue({
mockAppContext({
isCurrentWorkspaceManager: true,
workspacePermissionKeys: [],
})

View File

@ -2,11 +2,12 @@
import type { FC } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { toast } from '@langgenius/dify-ui/toast'
import { useAtomValue } from 'jotai'
import * as React from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { Azure, GoogleCloud } from '@/app/components/base/icons/src/public/billing'
import { useAppContext } from '@/context/app-context'
import { workspacePermissionKeysAtom } from '@/context/app-context-state'
import { BillingPermission, hasPermission } from '@/utils/permission'
import { contactSalesUrl, getStartedWithCommunityUrl, getWithPremiumUrl } from '../../../config'
import { SelfHostedPlan } from '../../../type'
@ -52,7 +53,7 @@ const SelfHostedPlanItem: FC<SelfHostedPlanItemProps> = ({
const isFreePlan = plan === SelfHostedPlan.community
const isPremiumPlan = plan === SelfHostedPlan.premium
const isEnterprisePlan = plan === SelfHostedPlan.enterprise
const { workspacePermissionKeys } = useAppContext()
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
const canManageBilling = hasPermission(workspacePermissionKeys, BillingPermission.Manage)
const handleGetPayUrl = useCallback(() => {

View File

@ -46,6 +46,10 @@ export const userProfileIdAtom = atom((get) => {
return get(userProfileAtom).id
})
export const userProfileEmailAtom = atom((get) => {
return get(userProfileAtom).email
})
const profileMetaAtom = atom((get) => {
const accountProfileQuery = get(accountProfileQueryAtom) as SuspenseQueryResult<UserProfileWithMeta>
@ -81,6 +85,10 @@ export const isCurrentWorkspaceOwnerAtom = atom((get) => {
return get(workspaceRoleFlagsAtom).isCurrentWorkspaceOwner
})
export const isCurrentWorkspaceManagerAtom = atom((get) => {
return get(workspaceRoleFlagsAtom).isCurrentWorkspaceManager
})
const workspacePermissionKeysQueryAtom = atomWithQuery((get) => {
const workspaceId = get(currentWorkspaceIdAtom)
@ -128,6 +136,10 @@ export const langGeniusVersionInfoAtom = atom((get) => {
})
})
export const langGeniusCurrentVersionAtom = atom((get) => {
return get(langGeniusVersionInfoAtom).current_version
})
export const refreshUserProfileAtom = atom(null, (get) => {
const queryClient = get(queryClientAtom)
queryClient.invalidateQueries({ queryKey: userProfileQueryOptions().queryKey })