fix(core): workspace billing cannot be opened (#9091)
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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} />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user