mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 18:58:35 +08:00
refactor(web): migrate billing app context consumers (#38541)
This commit is contained in:
parent
3523da508f
commit
5a210cf03e
@ -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,
|
||||
|
||||
@ -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',
|
||||
}))
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
}))
|
||||
|
||||
@ -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]}`)
|
||||
},
|
||||
}
|
||||
|
||||
@ -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')
|
||||
})
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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,
|
||||
}))
|
||||
|
||||
@ -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' })}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>()
|
||||
|
||||
@ -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: [],
|
||||
})
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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: [],
|
||||
})
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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 })
|
||||
|
||||
Loading…
Reference in New Issue
Block a user