mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-05 19:40:30 +08:00
feat: optional payment for frontend (#5056)
This commit is contained in:
@@ -28,6 +28,11 @@ const UserPlanButtonWithData = () => {
|
||||
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
if (plan === SubscriptionPlan.SelfHosted) {
|
||||
// Self hosted version doesn't have a payment apis.
|
||||
return <div className={styles.userPlanButton}>{plan}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip content={t['com.affine.payment.tag-tooltips']()} side="top">
|
||||
<div className={styles.userPlanButton} onClick={handleClick}>
|
||||
|
||||
@@ -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) => {
|
||||
</SettingWrapper>
|
||||
<SettingWrapper title={t['com.affine.brand.affineCloud']()}>
|
||||
<PublishPanel workspace={workspace} {...props} />
|
||||
<MembersPanel workspace={workspace} {...props} />
|
||||
<MembersPanel
|
||||
workspace={workspace}
|
||||
upgradable={!isSelfHosted}
|
||||
{...props}
|
||||
/>
|
||||
</SettingWrapper>
|
||||
{storageAndExportSetting}
|
||||
<SettingWrapper>
|
||||
|
||||
+14
-8
@@ -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,
|
||||
})}
|
||||
,
|
||||
<div className={style.goUpgradeWrapper} onClick={handleUpgrade}>
|
||||
<span className={style.goUpgrade}>
|
||||
{t['com.affine.payment.member.description.go-upgrade']()}
|
||||
</span>
|
||||
<ArrowRightBigIcon className={style.arrowRight} />
|
||||
</div>
|
||||
{upgradable ? (
|
||||
<>
|
||||
,
|
||||
<div className={style.goUpgradeWrapper} onClick={handleUpgrade}>
|
||||
<span className={style.goUpgrade}>
|
||||
{t['com.affine.payment.member.description.go-upgrade']()}
|
||||
</span>
|
||||
<ArrowRightBigIcon className={style.arrowRight} />
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}, [handleUpgrade, memberLimit, plan, t]);
|
||||
}, [handleUpgrade, memberLimit, plan, t, upgradable]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</SettingRow>
|
||||
);
|
||||
|
||||
+15
-12
@@ -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;
|
||||
|
||||
@@ -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<any> | 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);
|
||||
};
|
||||
@@ -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<SubscriptionQuery['currentUser']>['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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user