diff --git a/packages/backend/server/src/modules/index.ts b/packages/backend/server/src/modules/index.ts index 0239a5c5b6a70..a16215c68ebd2 100644 --- a/packages/backend/server/src/modules/index.ts +++ b/packages/backend/server/src/modules/index.ts @@ -6,6 +6,7 @@ import { GqlModule } from '../graphql.module'; import { ServerConfigModule } from './config'; import { DocModule } from './doc'; import { PaymentModule } from './payment'; +import { SelfHostedModule } from './self-hosted'; import { SyncModule } from './sync'; import { UsersModule } from './users'; import { WorkspaceModule } from './workspaces'; @@ -25,10 +26,12 @@ switch (SERVER_FLAVOR) { case 'selfhosted': BusinessModules.push( ServerConfigModule, + SelfHostedModule, ScheduleModule.forRoot(), GqlModule, WorkspaceModule, UsersModule, + SyncModule, DocModule.forRoot() ); break; diff --git a/packages/backend/server/src/modules/payment/resolver.ts b/packages/backend/server/src/modules/payment/resolver.ts index 913609a76dccb..7374f8e1d52d9 100644 --- a/packages/backend/server/src/modules/payment/resolver.ts +++ b/packages/backend/server/src/modules/payment/resolver.ts @@ -52,7 +52,7 @@ class SubscriptionPrice { } @ObjectType('UserSubscription') -class UserSubscriptionType implements Partial { +export class UserSubscriptionType implements Partial { @Field({ name: 'id' }) stripeSubscriptionId!: string; diff --git a/packages/backend/server/src/modules/payment/service.ts b/packages/backend/server/src/modules/payment/service.ts index 6dfbf19218643..c30ac03279aea 100644 --- a/packages/backend/server/src/modules/payment/service.ts +++ b/packages/backend/server/src/modules/payment/service.ts @@ -30,6 +30,7 @@ export enum SubscriptionPlan { Pro = 'pro', Team = 'team', Enterprise = 'enterprise', + SelfHosted = 'selfhosted', } export function encodeLookupKey( diff --git a/packages/backend/server/src/modules/self-hosted.ts b/packages/backend/server/src/modules/self-hosted.ts new file mode 100644 index 0000000000000..5bd684b981b34 --- /dev/null +++ b/packages/backend/server/src/modules/self-hosted.ts @@ -0,0 +1,38 @@ +import { Module } from '@nestjs/common'; +import { ResolveField, Resolver } from '@nestjs/graphql'; + +import { UserSubscriptionType } from './payment/resolver'; +import { + SubscriptionPlan, + SubscriptionRecurring, + SubscriptionStatus, +} from './payment/service'; +import { UserType } from './users'; + +const YEAR = 1000 * 60 * 60 * 24 * 30 * 12; + +@Resolver(() => UserType) +export class SelfHostedDummyResolver { + private readonly start = new Date(); + private readonly end = new Date(Number(this.start) + YEAR); + constructor() {} + + @ResolveField(() => UserSubscriptionType) + async subscription() { + return { + stripeSubscriptionId: 'dummy', + plan: SubscriptionPlan.SelfHosted, + recurring: SubscriptionRecurring.Yearly, + status: SubscriptionStatus.Active, + start: this.start, + end: this.end, + createdAt: this.start, + updatedAt: this.start, + }; + } +} + +@Module({ + providers: [SelfHostedDummyResolver], +}) +export class SelfHostedModule {} diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index cd76576875e00..68dc28c522ea1 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -81,6 +81,7 @@ enum SubscriptionPlan { Pro Team Enterprise + SelfHosted } type UserSubscription { diff --git a/packages/frontend/component/src/components/setting-components/storage-progess.tsx b/packages/frontend/component/src/components/setting-components/storage-progess.tsx index 4532a88ed064c..e98c564069898 100644 --- a/packages/frontend/component/src/components/setting-components/storage-progess.tsx +++ b/packages/frontend/component/src/components/setting-components/storage-progess.tsx @@ -11,6 +11,7 @@ import * as styles from './share.css'; export interface StorageProgressProgress { max: number; value: number; + upgradable?: boolean; onUpgrade: () => void; plan: SubscriptionPlan; } @@ -23,6 +24,7 @@ enum ButtonType { export const StorageProgress = ({ max: upperLimit, value, + upgradable = true, onUpgrade, plan, }: StorageProgressProgress) => { @@ -63,22 +65,24 @@ export const StorageProgress = ({ - - - - - + {upgradable ? ( + + + + + + ) : null} ); }; diff --git a/packages/frontend/core/src/components/affine/auth/user-plan-button.tsx b/packages/frontend/core/src/components/affine/auth/user-plan-button.tsx index aa0b417c6f853..dc1ecfdb291fd 100644 --- a/packages/frontend/core/src/components/affine/auth/user-plan-button.tsx +++ b/packages/frontend/core/src/components/affine/auth/user-plan-button.tsx @@ -28,6 +28,11 @@ const UserPlanButtonWithData = () => { const t = useAFFiNEI18N(); + if (plan === SubscriptionPlan.SelfHosted) { + // Self hosted version doesn't have a payment apis. + return
{plan}
; + } + return (
diff --git a/packages/frontend/core/src/components/affine/new-workspace-setting-detail/index.tsx b/packages/frontend/core/src/components/affine/new-workspace-setting-detail/index.tsx index 6732ce7b0e044..56af1e62ef45a 100644 --- a/packages/frontend/core/src/components/affine/new-workspace-setting-detail/index.tsx +++ b/packages/frontend/core/src/components/affine/new-workspace-setting-detail/index.tsx @@ -7,6 +7,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import { useMemo } from 'react'; +import { useSelfHosted } from '../../../hooks/affine/use-server-flavor'; import { useWorkspace } from '../../../hooks/use-workspace'; import { DeleteLeaveWorkspace } from './delete-leave-workspace'; import { ExportPanel } from './export'; @@ -20,6 +21,7 @@ import type { WorkspaceSettingDetailProps } from './types'; export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => { const { workspaceId } = props; const t = useAFFiNEI18N(); + const isSelfHosted = useSelfHosted(); const workspace = useWorkspace(workspaceId); const [name] = useBlockSuiteWorkspaceName(workspace.blockSuiteWorkspace); @@ -56,7 +58,11 @@ export const WorkspaceSettingDetail = (props: WorkspaceSettingDetailProps) => { - + {storageAndExportSetting} diff --git a/packages/frontend/core/src/components/affine/new-workspace-setting-detail/members.tsx b/packages/frontend/core/src/components/affine/new-workspace-setting-detail/members.tsx index 687e453467a86..55926dbb34a19 100644 --- a/packages/frontend/core/src/components/affine/new-workspace-setting-detail/members.tsx +++ b/packages/frontend/core/src/components/affine/new-workspace-setting-detail/members.tsx @@ -51,6 +51,7 @@ enum MemberLimitCount { const COUNT_PER_PAGE = 8; export interface MembersPanelProps extends WorkspaceSettingDetailProps { + upgradable: boolean; workspace: AffineOfficialWorkspace; } type OnRevoke = (memberId: string) => void; @@ -70,6 +71,7 @@ const MembersPanelLocal = () => { export const CloudWorkspaceMembersPanel = ({ workspace, isOwner, + upgradable, }: MembersPanelProps) => { const workspaceId = workspace.id; const memberCount = useMemberCount(workspaceId); @@ -165,16 +167,20 @@ export const CloudWorkspaceMembersPanel = ({ planName: plan, memberLimit, })} - , -
- - {t['com.affine.payment.member.description.go-upgrade']()} - - -
+ {upgradable ? ( + <> + , +
+ + {t['com.affine.payment.member.description.go-upgrade']()} + + +
+ + ) : null} ); - }, [handleUpgrade, memberLimit, plan, t]); + }, [handleUpgrade, memberLimit, plan, t, upgradable]); return ( <> diff --git a/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx index 2c16543363272..4d82b3c5d8b6e 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/account-setting/index.tsx @@ -35,6 +35,7 @@ import { openSignOutModalAtom, } from '../../../../atoms'; import { useCurrentUser } from '../../../../hooks/affine/use-current-user'; +import { useSelfHosted } from '../../../../hooks/affine/use-server-flavor'; import { useUserSubscription } from '../../../../hooks/use-subscription'; import { Upload } from '../../../pure/file-upload'; import * as style from './style.css'; @@ -167,6 +168,7 @@ export const AvatarAndName = () => { const StoragePanel = () => { const t = useAFFiNEI18N(); + const isSelfHosted = useSelfHosted(); const { data } = useQuery({ query: allBlobSizesQuery, @@ -175,6 +177,7 @@ const StoragePanel = () => { const [subscription] = useUserSubscription(); const plan = subscription?.plan ?? SubscriptionPlan.Free; + // TODO(@JimmFly): get limit from user usage query directly after #4720 is merged const maxLimit = useMemo(() => { return bytes.parse(plan === SubscriptionPlan.Free ? '10GB' : '100GB'); }, [plan]); @@ -199,6 +202,7 @@ const StoragePanel = () => { plan={plan} value={data.collectAllBlobSizes.size} onUpgrade={onUpgrade} + upgradable={!isSelfHosted} /> ); diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/index.tsx index 956d99bd5b2f6..2f63a3b1a4e31 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/index.tsx @@ -8,6 +8,7 @@ import { import type { ReactElement, SVGProps } from 'react'; import { useCurrentLoginStatus } from '../../../../hooks/affine/use-current-login-status'; +import { useSelfHosted } from '../../../../hooks/affine/use-server-flavor'; import { AboutAffine } from './about'; import { AppearanceSettings } from './appearance'; import { BillingSettings } from './billing'; @@ -36,6 +37,7 @@ export type GeneralSettingList = GeneralSettingListItem[]; export const useGeneralSettingList = (): GeneralSettingList => { const t = useAFFiNEI18N(); const status = useCurrentLoginStatus(); + const isSelfHosted = useSelfHosted(); const settings: GeneralSettingListItem[] = [ { @@ -50,13 +52,6 @@ export const useGeneralSettingList = (): GeneralSettingList => { icon: KeyboardIcon, testId: 'shortcuts-panel-trigger', }, - { - key: 'plans', - title: t['com.affine.payment.title'](), - icon: UpgradeIcon, - testId: 'plans-panel-trigger', - }, - { key: 'plugins', title: 'Plugins', @@ -71,13 +66,21 @@ export const useGeneralSettingList = (): GeneralSettingList => { }, ]; - if (status === 'authenticated') { + if (!isSelfHosted) { settings.splice(3, 0, { - key: 'billing', - title: t['com.affine.payment.billing-setting.title'](), - icon: PaymentIcon, - testId: 'billing-panel-trigger', + key: 'plans', + title: t['com.affine.payment.title'](), + icon: UpgradeIcon, + testId: 'plans-panel-trigger', }); + if (status === 'authenticated') { + settings.splice(3, 0, { + key: 'billing', + title: t['com.affine.payment.billing-setting.title'](), + icon: PaymentIcon, + testId: 'billing-panel-trigger', + }); + } } return settings; diff --git a/packages/frontend/core/src/hooks/affine/use-server-flavor.ts b/packages/frontend/core/src/hooks/affine/use-server-flavor.ts new file mode 100644 index 0000000000000..6ffb71257c533 --- /dev/null +++ b/packages/frontend/core/src/hooks/affine/use-server-flavor.ts @@ -0,0 +1,29 @@ +import { serverConfigQuery } from '@affine/graphql'; +import { useQuery } from '@affine/workspace/affine/gql'; +import type { BareFetcher, Middleware } from 'swr'; + +const wrappedFetcher = (fetcher: BareFetcher | null, ...args: any[]) => + fetcher?.(...args).catch(() => null); + +const errorHandler: Middleware = useSWRNext => (key, fetcher, config) => { + return useSWRNext(key, wrappedFetcher.bind(null, fetcher), config); +}; + +export const useServerFlavor = () => { + const { data: config, error } = useQuery( + { query: serverConfigQuery }, + { use: [errorHandler] } + ); + + if (error || !config) { + return 'local'; + } + + return config.serverConfig.flavor; +}; + +export const useSelfHosted = () => { + const serverFlavor = useServerFlavor(); + + return ['local', 'selfhosted'].includes(serverFlavor); +}; diff --git a/packages/frontend/core/src/hooks/use-subscription.ts b/packages/frontend/core/src/hooks/use-subscription.ts index bdc254c71dd21..9ec556cb79dd7 100644 --- a/packages/frontend/core/src/hooks/use-subscription.ts +++ b/packages/frontend/core/src/hooks/use-subscription.ts @@ -2,6 +2,8 @@ import { type SubscriptionQuery, subscriptionQuery } from '@affine/graphql'; import { useQuery } from '@affine/workspace/affine/gql'; import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks'; +import { useSelfHosted } from './affine/use-server-flavor'; + export type Subscription = NonNullable< NonNullable['subscription'] >; @@ -12,6 +14,7 @@ const selector = (data: SubscriptionQuery) => data.currentUser?.subscription ?? null; export const useUserSubscription = () => { + const isSelfHosted = useSelfHosted(); const { data, mutate } = useQuery({ query: subscriptionQuery, }); @@ -36,5 +39,9 @@ export const useUserSubscription = () => { [mutate] ); + if (isSelfHosted) { + return [selector(data), () => {}] as const; + } + return [selector(data), set] as const; }; diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts index e1d84f572baad..61b4b4020c9bc 100644 --- a/packages/frontend/graphql/src/schema.ts +++ b/packages/frontend/graphql/src/schema.ts @@ -62,6 +62,7 @@ export enum SubscriptionPlan { Enterprise = 'Enterprise', Free = 'Free', Pro = 'Pro', + SelfHosted = 'SelfHosted', Team = 'Team', }