Skip to content

Commit

Permalink
feat: optional payment for frontend (#5056)
Browse files Browse the repository at this point in the history
  • Loading branch information
darkskygit authored Nov 25, 2023
1 parent 13e7121 commit f04ec50
Show file tree
Hide file tree
Showing 14 changed files with 146 additions and 38 deletions.
3 changes: 3 additions & 0 deletions packages/backend/server/src/modules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -25,10 +26,12 @@ switch (SERVER_FLAVOR) {
case 'selfhosted':
BusinessModules.push(
ServerConfigModule,
SelfHostedModule,
ScheduleModule.forRoot(),
GqlModule,
WorkspaceModule,
UsersModule,
SyncModule,
DocModule.forRoot()
);
break;
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/server/src/modules/payment/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class SubscriptionPrice {
}

@ObjectType('UserSubscription')
class UserSubscriptionType implements Partial<UserSubscription> {
export class UserSubscriptionType implements Partial<UserSubscription> {
@Field({ name: 'id' })
stripeSubscriptionId!: string;

Expand Down
1 change: 1 addition & 0 deletions packages/backend/server/src/modules/payment/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export enum SubscriptionPlan {
Pro = 'pro',
Team = 'team',
Enterprise = 'enterprise',
SelfHosted = 'selfhosted',
}

export function encodeLookupKey(
Expand Down
38 changes: 38 additions & 0 deletions packages/backend/server/src/modules/self-hosted.ts
Original file line number Diff line number Diff line change
@@ -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 {}
1 change: 1 addition & 0 deletions packages/backend/server/src/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ enum SubscriptionPlan {
Pro
Team
Enterprise
SelfHosted
}

type UserSubscription {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as styles from './share.css';
export interface StorageProgressProgress {
max: number;
value: number;
upgradable?: boolean;
onUpgrade: () => void;
plan: SubscriptionPlan;
}
Expand All @@ -23,6 +24,7 @@ enum ButtonType {
export const StorageProgress = ({
max: upperLimit,
value,
upgradable = true,
onUpgrade,
plan,
}: StorageProgressProgress) => {
Expand Down Expand Up @@ -63,22 +65,24 @@ export const StorageProgress = ({
</div>
</div>

<Tooltip
options={{ hidden: percent < 100 }}
content={t['com.affine.storage.maximum-tips']()}
>
<span tabIndex={0}>
<Button
type={buttonType}
onClick={onUpgrade}
className={styles.storageButton}
>
{plan === 'Free'
? t['com.affine.storage.upgrade']()
: t['com.affine.storage.change-plan']()}
</Button>
</span>
</Tooltip>
{upgradable ? (
<Tooltip
options={{ hidden: percent < 100 }}
content={t['com.affine.storage.maximum-tips']()}
>
<span tabIndex={0}>
<Button
type={buttonType}
onClick={onUpgrade}
className={styles.storageButton}
>
{plan === 'Free'
? t['com.affine.storage.upgrade']()
: t['com.affine.storage.change-plan']()}
</Button>
</span>
</Tooltip>
) : null}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);

Expand Down Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -70,6 +71,7 @@ const MembersPanelLocal = () => {
export const CloudWorkspaceMembersPanel = ({
workspace,
isOwner,
upgradable,
}: MembersPanelProps) => {
const workspaceId = workspace.id;
const memberCount = useMemberCount(workspaceId);
Expand Down Expand Up @@ -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 (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -167,6 +168,7 @@ export const AvatarAndName = () => {

const StoragePanel = () => {
const t = useAFFiNEI18N();
const isSelfHosted = useSelfHosted();

const { data } = useQuery({
query: allBlobSizesQuery,
Expand All @@ -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]);
Expand All @@ -199,6 +202,7 @@ const StoragePanel = () => {
plan={plan}
value={data.collectAllBlobSizes.size}
onUpgrade={onUpgrade}
upgradable={!isSelfHosted}
/>
</SettingRow>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -36,6 +37,7 @@ export type GeneralSettingList = GeneralSettingListItem[];
export const useGeneralSettingList = (): GeneralSettingList => {
const t = useAFFiNEI18N();
const status = useCurrentLoginStatus();
const isSelfHosted = useSelfHosted();

const settings: GeneralSettingListItem[] = [
{
Expand All @@ -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',
Expand All @@ -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;
Expand Down
29 changes: 29 additions & 0 deletions packages/frontend/core/src/hooks/affine/use-server-flavor.ts
Original file line number Diff line number Diff line change
@@ -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);
};
7 changes: 7 additions & 0 deletions packages/frontend/core/src/hooks/use-subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
>;
Expand All @@ -12,6 +14,7 @@ const selector = (data: SubscriptionQuery) =>
data.currentUser?.subscription ?? null;

export const useUserSubscription = () => {
const isSelfHosted = useSelfHosted();
const { data, mutate } = useQuery({
query: subscriptionQuery,
});
Expand All @@ -36,5 +39,9 @@ export const useUserSubscription = () => {
[mutate]
);

if (isSelfHosted) {
return [selector(data), () => {}] as const;
}

return [selector(data), set] as const;
};
1 change: 1 addition & 0 deletions packages/frontend/graphql/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export enum SubscriptionPlan {
Enterprise = 'Enterprise',
Free = 'Free',
Pro = 'Pro',
SelfHosted = 'SelfHosted',
Team = 'Team',
}

Expand Down

0 comments on commit f04ec50

Please sign in to comment.