fix(core): workspace billing cannot be opened (#9091)

This commit is contained in:
JimmFly
2024-12-10 12:14:59 +00:00
parent 2f80b4f822
commit f63dacd553
7 changed files with 85 additions and 75 deletions

View File

@@ -8,8 +8,10 @@ import {
import { getUpgradeQuestionnaireLink } from '@affine/core/components/hooks/affine/use-subscription-notify'; import { getUpgradeQuestionnaireLink } from '@affine/core/components/hooks/affine/use-subscription-notify';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { useMutation } from '@affine/core/components/hooks/use-mutation'; import { useMutation } from '@affine/core/components/hooks/use-mutation';
import { useWorkspace } from '@affine/core/components/hooks/use-workspace';
import { import {
AuthService, AuthService,
SubscriptionService,
WorkspaceInvoicesService, WorkspaceInvoicesService,
WorkspaceSubscriptionService, WorkspaceSubscriptionService,
} from '@affine/core/modules/cloud'; } from '@affine/core/modules/cloud';
@@ -27,7 +29,7 @@ import {
FrameworkScope, FrameworkScope,
useLiveData, useLiveData,
useService, useService,
WorkspaceService, type WorkspaceMetadata,
} from '@toeverything/infra'; } from '@toeverything/infra';
import { cssVar } from '@toeverything/theme'; import { cssVar } from '@toeverything/theme';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
@@ -38,18 +40,29 @@ import {
} from '../../general-setting/plans/actions'; } from '../../general-setting/plans/actions';
import * as styles from './styles.css'; import * as styles from './styles.css';
export const WorkspaceSettingBilling = () => { export const WorkspaceSettingBilling = ({
workspaceMetadata,
}: {
workspaceMetadata: WorkspaceMetadata;
}) => {
// useWorkspace hook is a vary heavy operation here, but we need syncing name and avatar changes here,
// we don't have a better way to do this now
const workspace = useWorkspace(workspaceMetadata);
const t = useI18n(); const t = useI18n();
const workspace = useService(WorkspaceService).workspace;
const subscriptionService = useService(WorkspaceSubscriptionService); const subscriptionService = workspace?.scope.get(
const subscription = useLiveData( WorkspaceSubscriptionService
subscriptionService.subscription.subscription$
); );
const title = useLiveData(workspace.name$) || 'untitled'; const subscription = useLiveData(
subscriptionService?.subscription.subscription$
);
useEffect(() => {
subscriptionService?.subscription.revalidate();
}, [subscriptionService]);
if (workspace === null) { if (workspace === null) {
console.log('workspace is null', title);
return null; return null;
} }
@@ -83,17 +96,20 @@ export const WorkspaceSettingBilling = () => {
const TeamCard = () => { const TeamCard = () => {
const t = useI18n(); const t = useI18n();
const subscriptionService = useService(WorkspaceSubscriptionService); const workspaceSubscriptionService = useService(WorkspaceSubscriptionService);
const subscriptionService = useService(SubscriptionService);
const teamSubscription = useLiveData( const teamSubscription = useLiveData(
subscriptionService.subscription.subscription$ workspaceSubscriptionService.subscription.subscription$
); );
const teamPrices = useLiveData(subscriptionService.prices.teamPrice$); const teamPrices = useLiveData(subscriptionService.prices.teamPrice$);
const [openCancelModal, setOpenCancelModal] = useState(false); const [openCancelModal, setOpenCancelModal] = useState(false);
useEffect(() => { useEffect(() => {
subscriptionService.subscription.revalidate(); workspaceSubscriptionService.subscription.revalidate();
subscriptionService.prices.revalidate(); }, [workspaceSubscriptionService.subscription]);
}, [subscriptionService]);
const expiration = teamSubscription?.end; const expiration = teamSubscription?.end;
const nextBillingDate = teamSubscription?.nextBillAt; const nextBillingDate = teamSubscription?.nextBillAt;
const recurring = teamSubscription?.recurring; const recurring = teamSubscription?.recurring;

View File

@@ -31,7 +31,7 @@ export const WorkspaceSetting = ({
<WorkspaceSettingProperties workspaceMetadata={workspaceMetadata} /> <WorkspaceSettingProperties workspaceMetadata={workspaceMetadata} />
); );
case 'workspace:billing': case 'workspace:billing':
return <WorkspaceSettingBilling />; return <WorkspaceSettingBilling workspaceMetadata={workspaceMetadata} />;
} }
return null; return null;
}; };

View File

@@ -1,7 +1,5 @@
import { Button, Loading, notify } from '@affine/component'; import { Button, Loading, notify } from '@affine/component';
import { import {
InviteModal,
type InviteModalProps,
InviteTeamMemberModal, InviteTeamMemberModal,
type InviteTeamMemberModalProps, type InviteTeamMemberModalProps,
MemberLimitModal, MemberLimitModal,
@@ -98,25 +96,6 @@ export const CloudWorkspaceMembersPanel = ({
return success; return success;
}, [permissionService.permission]); }, [permissionService.permission]);
const onInviteConfirm = useCallback<InviteModalProps['onConfirm']>(
async ({ email, permission }) => {
setIsMutating(true);
const success = await permissionService.permission.inviteMember(
email,
permission,
true
);
if (success) {
notify.success({
title: t['Invitation sent'](),
message: t['Invitation sent hint'](),
});
setOpen(false);
}
setIsMutating(false);
},
[permissionService.permission, t]
);
const onInviteBatchConfirm = useCallback< const onInviteBatchConfirm = useCallback<
InviteTeamMemberModalProps['onConfirm'] InviteTeamMemberModalProps['onConfirm']
>( >(
@@ -208,18 +187,7 @@ export const CloudWorkspaceMembersPanel = ({
{isOwner ? ( {isOwner ? (
<> <>
<Button onClick={openModal}>{t['Invite Members']()}</Button> <Button onClick={openModal}>{t['Invite Members']()}</Button>
{isTeam ? ( {isLimited && !isTeam ? (
<InviteTeamMemberModal
open={open}
setOpen={setOpen}
onConfirm={onInviteBatchConfirm}
isMutating={isMutating}
copyTextToClipboard={copyTextToClipboard}
onGenerateInviteLink={onGenerateInviteLink}
onRevokeInviteLink={onRevokeInviteLink}
importCSV={<ImportCSV onImport={onImportCSV} />}
/>
) : isLimited ? (
<MemberLimitModal <MemberLimitModal
isFreePlan={!plan} isFreePlan={!plan}
open={open} open={open}
@@ -229,11 +197,15 @@ export const CloudWorkspaceMembersPanel = ({
onConfirm={handleUpgradeConfirm} onConfirm={handleUpgradeConfirm}
/> />
) : ( ) : (
<InviteModal <InviteTeamMemberModal
open={open} open={open}
setOpen={setOpen} setOpen={setOpen}
onConfirm={onInviteConfirm} onConfirm={onInviteBatchConfirm}
isMutating={isMutating} isMutating={isMutating}
copyTextToClipboard={copyTextToClipboard}
onGenerateInviteLink={onGenerateInviteLink}
onRevokeInviteLink={onRevokeInviteLink}
importCSV={<ImportCSV onImport={onImportCSV} />}
/> />
)} )}
</> </>

View File

@@ -14,7 +14,8 @@ import {
import { EMPTY, map, mergeMap } from 'rxjs'; import { EMPTY, map, mergeMap } from 'rxjs';
import { isBackendError, isNetworkError } from '../error'; import { isBackendError, isNetworkError } from '../error';
import type { InvoicesStore } from '../stores/invoices'; import type { WorkspaceServerService } from '../services/workspace-server';
import { InvoicesStore } from '../stores/invoices';
export type Invoice = NonNullable< export type Invoice = NonNullable<
InvoicesQuery['currentUser'] InvoicesQuery['currentUser']
@@ -22,12 +23,14 @@ export type Invoice = NonNullable<
export class WorkspaceInvoices extends Entity { export class WorkspaceInvoices extends Entity {
constructor( constructor(
private readonly store: InvoicesStore, private readonly workspaceService: WorkspaceService,
private readonly workspaceService: WorkspaceService private readonly workspaceServerService: WorkspaceServerService
) { ) {
super(); super();
} }
store = this.workspaceServerService.server?.scope.get(InvoicesStore);
pageNum$ = new LiveData(0); pageNum$ = new LiveData(0);
invoiceCount$ = new LiveData<number | undefined>(undefined); invoiceCount$ = new LiveData<number | undefined>(undefined);
pageInvoices$ = new LiveData<Invoice[] | undefined>(undefined); pageInvoices$ = new LiveData<Invoice[] | undefined>(undefined);
@@ -43,6 +46,9 @@ export class WorkspaceInvoices extends Entity {
(a, b) => a === b, (a, b) => a === b,
pageNum => { pageNum => {
return fromPromise(async signal => { return fromPromise(async signal => {
if (!this.store) {
throw new Error('No invoices store');
}
return this.store.fetchWorkspaceInvoices( return this.store.fetchWorkspaceInvoices(
pageNum * this.PAGE_SIZE, pageNum * this.PAGE_SIZE,
this.PAGE_SIZE, this.PAGE_SIZE,

View File

@@ -15,8 +15,8 @@ import {
import { EMPTY, mergeMap } from 'rxjs'; import { EMPTY, mergeMap } from 'rxjs';
import { isBackendError, isNetworkError } from '../error'; import { isBackendError, isNetworkError } from '../error';
import type { ServerService } from '../services/server'; import type { WorkspaceServerService } from '../services/workspace-server';
import type { SubscriptionStore } from '../stores/subscription'; import { SubscriptionStore } from '../stores/subscription';
export type SubscriptionType = NonNullable< export type SubscriptionType = NonNullable<
SubscriptionQuery['currentUser'] SubscriptionQuery['currentUser']
@@ -33,18 +33,25 @@ export class WorkspaceSubscription extends Entity {
constructor( constructor(
private readonly workspaceService: WorkspaceService, private readonly workspaceService: WorkspaceService,
private readonly serverService: ServerService, private readonly workspaceServerService: WorkspaceServerService
private readonly store: SubscriptionStore
) { ) {
super(); super();
} }
server = this.workspaceServerService.server;
store = this.workspaceServerService.server?.scope.get(SubscriptionStore);
async resumeSubscription(idempotencyKey: string, plan?: SubscriptionPlan) { async resumeSubscription(idempotencyKey: string, plan?: SubscriptionPlan) {
if (!this.store) {
throw new Error('Subscription store not available');
}
await this.store.mutateResumeSubscription(idempotencyKey, plan); await this.store.mutateResumeSubscription(idempotencyKey, plan);
await this.waitForRevalidation(); await this.waitForRevalidation();
} }
async cancelSubscription(idempotencyKey: string, plan?: SubscriptionPlan) { async cancelSubscription(idempotencyKey: string, plan?: SubscriptionPlan) {
if (!this.store) {
throw new Error('Subscription store not available');
}
await this.store.mutateCancelSubscription(idempotencyKey, plan); await this.store.mutateCancelSubscription(idempotencyKey, plan);
await this.waitForRevalidation(); await this.waitForRevalidation();
} }
@@ -54,6 +61,9 @@ export class WorkspaceSubscription extends Entity {
recurring: SubscriptionRecurring, recurring: SubscriptionRecurring,
plan?: SubscriptionPlan plan?: SubscriptionPlan
) { ) {
if (!this.store) {
throw new Error('Subscription store not available');
}
await this.store.setSubscriptionRecurring(idempotencyKey, recurring, plan); await this.store.setSubscriptionRecurring(idempotencyKey, recurring, plan);
await this.waitForRevalidation(); await this.waitForRevalidation();
} }
@@ -70,11 +80,11 @@ export class WorkspaceSubscription extends Entity {
exhaustMapWithTrailing(() => { exhaustMapWithTrailing(() => {
return fromPromise(async signal => { return fromPromise(async signal => {
const currentWorkspaceId = this.workspaceService.workspace.id; const currentWorkspaceId = this.workspaceService.workspace.id;
if (!currentWorkspaceId) { if (!currentWorkspaceId || !this.server) {
return undefined; // no subscription if no user return undefined; // no subscription if no user
} }
const serverConfig =
await this.serverService.server.features$.waitForNonNull(signal); const serverConfig = await this.server.features$.waitForNonNull(signal);
if (!serverConfig.payment) { if (!serverConfig.payment) {
// No payment feature, no subscription // No payment feature, no subscription
@@ -83,6 +93,12 @@ export class WorkspaceSubscription extends Entity {
subscription: null, subscription: null,
}; };
} }
if (!this.store) {
return {
workspaceId: currentWorkspaceId,
subscription: null,
};
}
const { workspaceId, subscription } = const { workspaceId, subscription } =
await this.store.fetchWorkspaceSubscriptions( await this.store.fetchWorkspaceSubscriptions(
currentWorkspaceId, currentWorkspaceId,
@@ -101,7 +117,7 @@ export class WorkspaceSubscription extends Entity {
when: isBackendError, when: isBackendError,
}), }),
mergeMap(data => { mergeMap(data => {
if (data && data.subscription && data.workspaceId) { if (data && data.subscription && data.workspaceId && this.store) {
this.store.setCachedWorkspaceSubscription( this.store.setCachedWorkspaceSubscription(
data.workspaceId, data.workspaceId,
data.subscription data.subscription

View File

@@ -149,16 +149,7 @@ export function configureCloudModule(framework: Framework) {
.store(UserFeatureStore, [GraphQLService]) .store(UserFeatureStore, [GraphQLService])
.service(InvoicesService) .service(InvoicesService)
.store(InvoicesStore, [GraphQLService]) .store(InvoicesStore, [GraphQLService])
.entity(Invoices, [InvoicesStore]) .entity(Invoices, [InvoicesStore]);
.scope(WorkspaceScope)
.service(WorkspaceSubscriptionService, [SubscriptionStore])
.entity(WorkspaceSubscription, [
WorkspaceService,
ServerService,
SubscriptionStore,
])
.service(WorkspaceInvoicesService)
.entity(WorkspaceInvoices, [InvoicesStore, WorkspaceService]);
framework framework
.scope(WorkspaceScope) .scope(WorkspaceScope)
@@ -167,4 +158,10 @@ export function configureCloudModule(framework: Framework) {
.service(CloudDocMetaService) .service(CloudDocMetaService)
.entity(CloudDocMeta, [CloudDocMetaStore, DocService, GlobalCache]) .entity(CloudDocMeta, [CloudDocMetaStore, DocService, GlobalCache])
.store(CloudDocMetaStore, [WorkspaceServerService]); .store(CloudDocMetaStore, [WorkspaceServerService]);
framework
.scope(WorkspaceScope)
.service(WorkspaceSubscriptionService, [WorkspaceServerService])
.entity(WorkspaceSubscription, [WorkspaceService, WorkspaceServerService])
.service(WorkspaceInvoicesService)
.entity(WorkspaceInvoices, [WorkspaceService, WorkspaceServerService]);
} }

View File

@@ -1,19 +1,22 @@
import { type CreateCheckoutSessionInput } from '@affine/graphql'; import { type CreateCheckoutSessionInput } from '@affine/graphql';
import { Service } from '@toeverything/infra'; import { Service } from '@toeverything/infra';
import { SubscriptionPrices } from '../entities/subscription-prices';
import { WorkspaceSubscription } from '../entities/workspace-subscription'; import { WorkspaceSubscription } from '../entities/workspace-subscription';
import type { SubscriptionStore } from '../stores/subscription'; import { SubscriptionStore } from '../stores/subscription';
import type { WorkspaceServerService } from './workspace-server';
export class WorkspaceSubscriptionService extends Service { export class WorkspaceSubscriptionService extends Service {
subscription = this.framework.createEntity(WorkspaceSubscription); subscription = this.framework.createEntity(WorkspaceSubscription);
prices = this.framework.createEntity(SubscriptionPrices);
constructor(private readonly store: SubscriptionStore) { constructor(private readonly workspaceServerService: WorkspaceServerService) {
super(); super();
} }
store = this.workspaceServerService.server?.scope.get(SubscriptionStore);
async createCheckoutSession(input: CreateCheckoutSessionInput) { async createCheckoutSession(input: CreateCheckoutSessionInput) {
if (!this.store) {
throw new Error('No subscription store');
}
return await this.store.createCheckoutSession(input); return await this.store.createCheckoutSession(input);
} }
} }