diff --git a/packages/backend/server/src/plugins/payment/resolver.ts b/packages/backend/server/src/plugins/payment/resolver.ts index d8200f01b8..968b66438e 100644 --- a/packages/backend/server/src/plugins/payment/resolver.ts +++ b/packages/backend/server/src/plugins/payment/resolver.ts @@ -56,7 +56,10 @@ export class UserSubscriptionType implements Partial { @Field({ name: 'id' }) stripeSubscriptionId!: string; - @Field(() => SubscriptionPlan) + @Field(() => SubscriptionPlan, { + description: + "The 'Free' plan just exists to be a placeholder and for the type convenience of frontend.\nThere won't actually be a subscription with plan 'Free'", + }) plan!: SubscriptionPlan; @Field(() => SubscriptionRecurring) diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 0955a11063..2a9bd40c75 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -443,6 +443,11 @@ type UserSubscription { end: DateTime! id: String! nextBillAt: DateTime + + """ + The 'Free' plan just exists to be a placeholder and for the type convenience of frontend. + There won't actually be a subscription with plan 'Free' + """ plan: SubscriptionPlan! recurring: SubscriptionRecurring! start: DateTime! 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 481b362da8..49cfb58d1a 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 @@ -21,7 +21,7 @@ export const UserPlanButton = () => { serverConfigService.serverConfig.features$.map(r => r?.payment) ); const plan = useLiveData( - subscriptionService.subscription.primary$.map(subscription => + subscriptionService.subscription.pro$.map(subscription => subscription !== null ? subscription?.plan : null ) ); diff --git a/packages/frontend/core/src/components/affine/setting-modal/account-setting/storage-progress.tsx b/packages/frontend/core/src/components/affine/setting-modal/account-setting/storage-progress.tsx index 7c3683d698..dd25a17045 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/account-setting/storage-progress.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/account-setting/storage-progress.tsx @@ -1,5 +1,4 @@ import { Button, ErrorMessage, Skeleton, Tooltip } from '@affine/component'; -import { SubscriptionPlan } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useLiveData, useService } from '@toeverything/infra'; import { cssVar } from '@toeverything/theme'; @@ -45,15 +44,14 @@ export const StorageProgress = ({ onUpgrade }: StorageProgressProgress) => { subscription.revalidate(); }, [subscription]); - const primarySubscription = useLiveData(subscription.primary$); - const isFreeUser = - !primarySubscription || primarySubscription?.plan === SubscriptionPlan.Free; + const proSubscription = useLiveData(subscription.pro$); + const isFreeUser = !proSubscription; const quotaName = useLiveData( quota.quota$.map(q => (q !== null ? q?.humanReadable.name : null)) ); const loading = - primarySubscription === null || percent === null || quotaName === null; + proSubscription === null || percent === null || quotaName === null; const loadError = useLiveData(quota.error$); const buttonType = useMemo(() => { diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx index 4fd17971c3..7a0c8195e7 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/billing/index.tsx @@ -96,47 +96,39 @@ const SubscriptionSettings = () => { subscriptionService.prices.revalidate(); }, [subscriptionService]); - const primarySubscription = useLiveData( - subscriptionService.subscription.primary$ - ); + const proSubscription = useLiveData(subscriptionService.subscription.pro$); const proPrice = useLiveData(subscriptionService.prices.proPrice$); - const currentPlan = - primarySubscription === null - ? null - : primarySubscription?.plan ?? SubscriptionPlan.Free; - const [openCancelModal, setOpenCancelModal] = useState(false); - - const recurring = - primarySubscription?.recurring ?? SubscriptionRecurring.Monthly; - const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom); + const currentPlan = proSubscription?.plan ?? SubscriptionPlan.Free; + const currentRecurring = + proSubscription?.recurring ?? SubscriptionRecurring.Monthly; + const gotoPlansSetting = useCallback(() => { mixpanel.track('Button', { resolve: 'ChangePlan', - currentPlan: currentPlan, + currentPlan: proSubscription?.plan, }); setOpenSettingModalAtom({ open: true, activeTab: 'plans', }); - }, [currentPlan, setOpenSettingModalAtom]); + }, [proSubscription, setOpenSettingModalAtom]); - const amount = currentPlan - ? currentPlan === SubscriptionPlan.Free - ? '0' - : proPrice - ? recurring === SubscriptionRecurring.Monthly - ? String((proPrice.amount ?? 0) / 100) - : String((proPrice.yearlyAmount ?? 0) / 100) - : '?' - : '?'; + const amount = proSubscription + ? proPrice + ? proSubscription.recurring === SubscriptionRecurring.Monthly + ? String((proPrice.amount ?? 0) / 100) + : String((proPrice.yearlyAmount ?? 0) / 100) + : '?' + : '0'; return (
- {currentPlan !== null ? ( + {/* loaded */} + {proSubscription !== null ? (
{ name={t['com.affine.payment.billing-setting.current-plan']()} desc={ { ${amount} / - {recurring === SubscriptionRecurring.Monthly + {currentRecurring === SubscriptionRecurring.Monthly ? t['com.affine.payment.billing-setting.month']() : t['com.affine.payment.billing-setting.year']()} @@ -179,8 +171,8 @@ const SubscriptionSettings = () => { )} - {primarySubscription !== null ? ( - primarySubscription?.status === SubscriptionStatus.Active && ( + {proSubscription !== null ? ( + proSubscription?.status === SubscriptionStatus.Active && ( <> { > - {primarySubscription.nextBillAt && ( + {proSubscription.nextBillAt && ( )} - {primarySubscription.canceledAt ? ( + {proSubscription.canceledAt ? ( @@ -322,9 +314,9 @@ const PlanAction = ({ type="primary" onClick={gotoPlansSetting} > - {plan === SubscriptionPlan.Free - ? t['com.affine.payment.billing-setting.upgrade']() - : t['com.affine.payment.billing-setting.change-plan']()} + {plan === SubscriptionPlan.Pro + ? t['com.affine.payment.billing-setting.change-plan']() + : t['com.affine.payment.billing-setting.upgrade']()} ); }; diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx index 77486bcbae..ab2a22eb7f 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/index.tsx @@ -36,9 +36,7 @@ const Settings = () => { const scrollWrapper = useRef(null); const subscriptionService = useService(SubscriptionService); - const primarySubscription = useLiveData( - subscriptionService.subscription.primary$ - ); + const proSubscription = useLiveData(subscriptionService.subscription.pro$); const prices = useLiveData(subscriptionService.prices.prices$); useEffect(() => { @@ -62,13 +60,13 @@ const Settings = () => { }); const [recurring, setRecurring] = useState( - primarySubscription?.recurring ?? SubscriptionRecurring.Yearly + proSubscription?.recurring ?? SubscriptionRecurring.Yearly ); - const currentPlan = primarySubscription?.plan ?? SubscriptionPlan.Free; - const isCanceled = !!primarySubscription?.canceledAt; + const currentPlan = proSubscription?.plan ?? SubscriptionPlan.Free; + const isCanceled = !!proSubscription?.canceledAt; const currentRecurring = - primarySubscription?.recurring ?? SubscriptionRecurring.Monthly; + proSubscription?.recurring ?? SubscriptionRecurring.Monthly; const yearlyDiscount = ( planDetail.get(SubscriptionPlan.Pro) as FixedPrice | undefined diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx index 22d488afa5..911e637d65 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/plans/plan-card.tsx @@ -32,15 +32,13 @@ export const PlanCard = (props: PlanCardProps) => { const loggedIn = useLiveData(useService(AuthService).session.status$) === 'authenticated'; const subscriptionService = useService(SubscriptionService); - const primarySubscription = useLiveData( - subscriptionService.subscription.primary$ - ); - const currentPlan = primarySubscription?.plan ?? SubscriptionPlan.Free; + const proSubscription = useLiveData(subscriptionService.subscription.pro$); + const currentPlan = proSubscription?.plan ?? SubscriptionPlan.Free; const isCurrent = loggedIn && detail.plan === currentPlan && - recurring === primarySubscription?.recurring; + recurring === proSubscription?.recurring; const isPro = detail.plan === SubscriptionPlan.Pro; return ( @@ -93,7 +91,7 @@ const ActionButton = ({ detail, recurring }: PlanCardProps) => { useLiveData(useService(AuthService).session.status$) === 'authenticated'; const subscriptionService = useService(SubscriptionService); const primarySubscription = useLiveData( - subscriptionService.subscription.primary$ + subscriptionService.subscription.pro$ ); const currentPlan = primarySubscription?.plan ?? SubscriptionPlan.Free; const currentRecurring = primarySubscription?.recurring; diff --git a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/members.tsx b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/members.tsx index 4dffee5a1b..b0385972ec 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/members.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/workspace-setting/new-workspace-setting-detail/members.tsx @@ -24,7 +24,7 @@ import { useRevokeMemberPermission } from '@affine/core/hooks/affine/use-revoke- import { WorkspacePermissionService } from '@affine/core/modules/permissions'; import { WorkspaceQuotaService } from '@affine/core/modules/quota'; import { WorkspaceFlavour } from '@affine/env/workspace'; -import { Permission, SubscriptionPlan } from '@affine/graphql'; +import { Permission } from '@affine/graphql'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { MoreVerticalIcon } from '@blocksuite/icons'; import { @@ -98,7 +98,7 @@ export const CloudWorkspaceMembersPanel = () => { const workspaceQuota = useLiveData(workspaceQuotaService.quota.quota$); const subscriptionService = useService(SubscriptionService); const plan = useLiveData( - subscriptionService.subscription.primary$.map(s => s?.plan) + subscriptionService.subscription.pro$.map(s => s?.plan) ); const isLimited = workspaceQuota ? checkMemberCountLimit(memberCount, workspaceQuota.memberLimit) @@ -206,7 +206,7 @@ export const CloudWorkspaceMembersPanel = () => { {isLimited ? ( { useEffect(() => { subscription.revalidate(); }, [subscription]); - const primary = useLiveData(subscription.primary$); - const plan = primary?.plan; + const plan = useLiveData(subscription.pro$)?.plan; if (!user) { // TODO: loading UI diff --git a/packages/frontend/core/src/modules/cloud/entities/subscription.ts b/packages/frontend/core/src/modules/cloud/entities/subscription.ts index e76bd5f42a..a8d20aa70a 100644 --- a/packages/frontend/core/src/modules/cloud/entities/subscription.ts +++ b/packages/frontend/core/src/modules/cloud/entities/subscription.ts @@ -28,34 +28,11 @@ export class Subscription extends Entity { isRevalidating$ = new LiveData(false); error$ = new LiveData(null); - /** - * Primary subscription is the subscription that is not AI. - */ - primary$ = this.subscription$.map(subscriptions => - subscriptions - ? subscriptions.find(sub => sub.plan !== SubscriptionPlan.AI) - : null - ); - isFree$ = this.subscription$.map(subscriptions => - subscriptions - ? subscriptions.some(sub => sub.plan === SubscriptionPlan.Free) - : null - ); - isPro$ = this.subscription$.map(subscriptions => - subscriptions - ? subscriptions.some(sub => sub.plan === SubscriptionPlan.Pro) - : null - ); pro$ = this.subscription$.map(subscriptions => subscriptions ? subscriptions.find(sub => sub.plan === SubscriptionPlan.Pro) : null ); - isSelfHosted$ = this.subscription$.map(subscriptions => - subscriptions - ? subscriptions.some(sub => sub.plan === SubscriptionPlan.SelfHosted) - : null - ); ai$ = this.subscription$.map(subscriptions => subscriptions ? subscriptions.find(sub => sub.plan === SubscriptionPlan.AI)