mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 18:26:05 +08:00
feat(core): add workspace billing (#9043)
This commit is contained in:
@@ -8,7 +8,11 @@ import { nanoid } from 'nanoid';
|
|||||||
import type { PropsWithChildren } from 'react';
|
import type { PropsWithChildren } from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { AuthService, SubscriptionService } from '../../../../../modules/cloud';
|
import {
|
||||||
|
AuthService,
|
||||||
|
SubscriptionService,
|
||||||
|
WorkspaceSubscriptionService,
|
||||||
|
} from '../../../../../modules/cloud';
|
||||||
import { ConfirmLoadingModal, DowngradeModal } from './modals';
|
import { ConfirmLoadingModal, DowngradeModal } from './modals';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -101,15 +105,15 @@ export const CancelTeamAction = ({
|
|||||||
} & PropsWithChildren) => {
|
} & PropsWithChildren) => {
|
||||||
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
const [idempotencyKey, setIdempotencyKey] = useState(nanoid());
|
||||||
const [isMutating, setIsMutating] = useState(false);
|
const [isMutating, setIsMutating] = useState(false);
|
||||||
const subscription = useService(SubscriptionService).subscription;
|
const subscription = useService(WorkspaceSubscriptionService).subscription;
|
||||||
const teamSubscription = useLiveData(subscription.team$);
|
const workspaceSubscription = useLiveData(subscription.subscription$);
|
||||||
const authService = useService(AuthService);
|
const authService = useService(AuthService);
|
||||||
const downgradeNotify = useDowngradeNotify();
|
const downgradeNotify = useDowngradeNotify();
|
||||||
|
|
||||||
const downgrade = useAsyncCallback(async () => {
|
const downgrade = useAsyncCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const account = authService.session.account$.value;
|
const account = authService.session.account$.value;
|
||||||
const prevRecurring = teamSubscription?.recurring;
|
const prevRecurring = workspaceSubscription?.recurring;
|
||||||
setIsMutating(true);
|
setIsMutating(true);
|
||||||
await subscription.cancelSubscription(idempotencyKey);
|
await subscription.cancelSubscription(idempotencyKey);
|
||||||
await subscription.waitForRevalidation();
|
await subscription.waitForRevalidation();
|
||||||
@@ -133,7 +137,7 @@ export const CancelTeamAction = ({
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
authService.session.account$.value,
|
authService.session.account$.value,
|
||||||
teamSubscription,
|
workspaceSubscription,
|
||||||
subscription,
|
subscription,
|
||||||
idempotencyKey,
|
idempotencyKey,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hoo
|
|||||||
import { useMutation } from '@affine/core/components/hooks/use-mutation';
|
import { useMutation } from '@affine/core/components/hooks/use-mutation';
|
||||||
import {
|
import {
|
||||||
AuthService,
|
AuthService,
|
||||||
InvoicesService,
|
WorkspaceInvoicesService,
|
||||||
SubscriptionService,
|
WorkspaceSubscriptionService,
|
||||||
} from '@affine/core/modules/cloud';
|
} from '@affine/core/modules/cloud';
|
||||||
import { UrlService } from '@affine/core/modules/url';
|
import { UrlService } from '@affine/core/modules/url';
|
||||||
import {
|
import {
|
||||||
@@ -41,8 +41,10 @@ import * as styles from './styles.css';
|
|||||||
export const WorkspaceSettingBilling = () => {
|
export const WorkspaceSettingBilling = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
const workspace = useService(WorkspaceService).workspace;
|
||||||
const subscriptionService = useService(SubscriptionService);
|
const subscriptionService = useService(WorkspaceSubscriptionService);
|
||||||
const team = useLiveData(subscriptionService.subscription.team$);
|
const subscription = useLiveData(
|
||||||
|
subscriptionService.subscription.subscription$
|
||||||
|
);
|
||||||
const title = useLiveData(workspace.name$) || 'untitled';
|
const title = useLiveData(workspace.name$) || 'untitled';
|
||||||
|
|
||||||
if (workspace === null) {
|
if (workspace === null) {
|
||||||
@@ -51,7 +53,7 @@ export const WorkspaceSettingBilling = () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!team) {
|
if (!subscription) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,8 +69,8 @@ export const WorkspaceSettingBilling = () => {
|
|||||||
<TeamCard />
|
<TeamCard />
|
||||||
<TypeFormLink />
|
<TypeFormLink />
|
||||||
<PaymentMethodUpdater />
|
<PaymentMethodUpdater />
|
||||||
{team.end && team.canceledAt ? (
|
{subscription?.end && subscription.canceledAt ? (
|
||||||
<ResumeSubscription expirationDate={team.end} />
|
<ResumeSubscription expirationDate={subscription.end} />
|
||||||
) : null}
|
) : null}
|
||||||
</SettingWrapper>
|
</SettingWrapper>
|
||||||
|
|
||||||
@@ -81,8 +83,10 @@ export const WorkspaceSettingBilling = () => {
|
|||||||
|
|
||||||
const TeamCard = () => {
|
const TeamCard = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const subscriptionService = useService(SubscriptionService);
|
const subscriptionService = useService(WorkspaceSubscriptionService);
|
||||||
const teamSubscription = useLiveData(subscriptionService.subscription.team$);
|
const teamSubscription = useLiveData(
|
||||||
|
subscriptionService.subscription.subscription$
|
||||||
|
);
|
||||||
const teamPrices = useLiveData(subscriptionService.prices.teamPrice$);
|
const teamPrices = useLiveData(subscriptionService.prices.teamPrice$);
|
||||||
|
|
||||||
const [openCancelModal, setOpenCancelModal] = useState(false);
|
const [openCancelModal, setOpenCancelModal] = useState(false);
|
||||||
@@ -198,23 +202,22 @@ const ResumeSubscription = ({ expirationDate }: { expirationDate: string }) => {
|
|||||||
|
|
||||||
const TypeFormLink = () => {
|
const TypeFormLink = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const subscriptionService = useService(SubscriptionService);
|
const workspaceSubscriptionService = useService(WorkspaceSubscriptionService);
|
||||||
const authService = useService(AuthService);
|
const authService = useService(AuthService);
|
||||||
|
|
||||||
const team = useLiveData(subscriptionService.subscription.team$);
|
const workspaceSubscription = useLiveData(
|
||||||
|
workspaceSubscriptionService.subscription.subscription$
|
||||||
|
);
|
||||||
const account = useLiveData(authService.session.account$);
|
const account = useLiveData(authService.session.account$);
|
||||||
|
|
||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
|
|
||||||
const plan = [];
|
|
||||||
if (team) plan.push(SubscriptionPlan.Team);
|
|
||||||
|
|
||||||
const link = getUpgradeQuestionnaireLink({
|
const link = getUpgradeQuestionnaireLink({
|
||||||
name: account.info?.name,
|
name: account.info?.name,
|
||||||
id: account.id,
|
id: account.id,
|
||||||
email: account.email,
|
email: account.email,
|
||||||
recurring: team?.recurring ?? SubscriptionRecurring.Yearly,
|
recurring: workspaceSubscription?.recurring ?? SubscriptionRecurring.Yearly,
|
||||||
plan,
|
plan: SubscriptionPlan.Team,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -263,7 +266,7 @@ const PaymentMethodUpdater = () => {
|
|||||||
const BillingHistory = () => {
|
const BillingHistory = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
const invoicesService = useService(InvoicesService);
|
const invoicesService = useService(WorkspaceInvoicesService);
|
||||||
const pageInvoices = useLiveData(invoicesService.invoices.pageInvoices$);
|
const pageInvoices = useLiveData(invoicesService.invoices.pageInvoices$);
|
||||||
const invoiceCount = useLiveData(invoicesService.invoices.invoiceCount$);
|
const invoiceCount = useLiveData(invoicesService.invoices.invoiceCount$);
|
||||||
const isLoading = useLiveData(invoicesService.invoices.isLoading$);
|
const isLoading = useLiveData(invoicesService.invoices.isLoading$);
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import type { InvoicesQuery } from '@affine/graphql';
|
||||||
|
import type { WorkspaceService } from '@toeverything/infra';
|
||||||
|
import {
|
||||||
|
backoffRetry,
|
||||||
|
catchErrorInto,
|
||||||
|
effect,
|
||||||
|
Entity,
|
||||||
|
exhaustMapSwitchUntilChanged,
|
||||||
|
fromPromise,
|
||||||
|
LiveData,
|
||||||
|
onComplete,
|
||||||
|
onStart,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import { EMPTY, map, mergeMap } from 'rxjs';
|
||||||
|
|
||||||
|
import { isBackendError, isNetworkError } from '../error';
|
||||||
|
import type { InvoicesStore } from '../stores/invoices';
|
||||||
|
|
||||||
|
export type Invoice = NonNullable<
|
||||||
|
InvoicesQuery['currentUser']
|
||||||
|
>['invoices'][number];
|
||||||
|
|
||||||
|
export class WorkspaceInvoices extends Entity {
|
||||||
|
constructor(
|
||||||
|
private readonly store: InvoicesStore,
|
||||||
|
private readonly workspaceService: WorkspaceService
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
pageNum$ = new LiveData(0);
|
||||||
|
invoiceCount$ = new LiveData<number | undefined>(undefined);
|
||||||
|
pageInvoices$ = new LiveData<Invoice[] | undefined>(undefined);
|
||||||
|
|
||||||
|
isLoading$ = new LiveData(false);
|
||||||
|
error$ = new LiveData<any>(null);
|
||||||
|
|
||||||
|
readonly PAGE_SIZE = 8;
|
||||||
|
|
||||||
|
readonly revalidate = effect(
|
||||||
|
map(() => this.pageNum$.value),
|
||||||
|
exhaustMapSwitchUntilChanged(
|
||||||
|
(a, b) => a === b,
|
||||||
|
pageNum => {
|
||||||
|
return fromPromise(async signal => {
|
||||||
|
return this.store.fetchWorkspaceInvoices(
|
||||||
|
pageNum * this.PAGE_SIZE,
|
||||||
|
this.PAGE_SIZE,
|
||||||
|
this.workspaceService.workspace.id,
|
||||||
|
signal
|
||||||
|
);
|
||||||
|
}).pipe(
|
||||||
|
mergeMap(data => {
|
||||||
|
this.invoiceCount$.setValue(data.invoiceCount);
|
||||||
|
this.pageInvoices$.setValue(data.invoices);
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
backoffRetry({
|
||||||
|
when: isNetworkError,
|
||||||
|
count: Infinity,
|
||||||
|
}),
|
||||||
|
backoffRetry({
|
||||||
|
when: isBackendError,
|
||||||
|
}),
|
||||||
|
catchErrorInto(this.error$),
|
||||||
|
onStart(() => {
|
||||||
|
this.pageInvoices$.setValue(undefined);
|
||||||
|
this.isLoading$.setValue(true);
|
||||||
|
}),
|
||||||
|
onComplete(() => this.isLoading$.setValue(false))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
setPageNum(pageNum: number) {
|
||||||
|
this.pageNum$.setValue(pageNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
override dispose(): void {
|
||||||
|
this.revalidate.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import type { SubscriptionQuery, SubscriptionRecurring } from '@affine/graphql';
|
||||||
|
import { SubscriptionPlan } from '@affine/graphql';
|
||||||
|
import type { WorkspaceService } from '@toeverything/infra';
|
||||||
|
import {
|
||||||
|
backoffRetry,
|
||||||
|
catchErrorInto,
|
||||||
|
effect,
|
||||||
|
Entity,
|
||||||
|
exhaustMapWithTrailing,
|
||||||
|
fromPromise,
|
||||||
|
LiveData,
|
||||||
|
onComplete,
|
||||||
|
onStart,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import { EMPTY, mergeMap } from 'rxjs';
|
||||||
|
|
||||||
|
import { isBackendError, isNetworkError } from '../error';
|
||||||
|
import type { ServerService } from '../services/server';
|
||||||
|
import type { SubscriptionStore } from '../stores/subscription';
|
||||||
|
|
||||||
|
export type SubscriptionType = NonNullable<
|
||||||
|
SubscriptionQuery['currentUser']
|
||||||
|
>['subscriptions'][number];
|
||||||
|
|
||||||
|
export class WorkspaceSubscription extends Entity {
|
||||||
|
subscription$ = new LiveData<SubscriptionType | null | undefined>(null);
|
||||||
|
isRevalidating$ = new LiveData(false);
|
||||||
|
error$ = new LiveData<any | null>(null);
|
||||||
|
|
||||||
|
team$ = this.subscription$.map(
|
||||||
|
subscription => subscription?.plan === SubscriptionPlan.Team
|
||||||
|
);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly workspaceService: WorkspaceService,
|
||||||
|
private readonly serverService: ServerService,
|
||||||
|
private readonly store: SubscriptionStore
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async resumeSubscription(idempotencyKey: string, plan?: SubscriptionPlan) {
|
||||||
|
await this.store.mutateResumeSubscription(idempotencyKey, plan);
|
||||||
|
await this.waitForRevalidation();
|
||||||
|
}
|
||||||
|
|
||||||
|
async cancelSubscription(idempotencyKey: string, plan?: SubscriptionPlan) {
|
||||||
|
await this.store.mutateCancelSubscription(idempotencyKey, plan);
|
||||||
|
await this.waitForRevalidation();
|
||||||
|
}
|
||||||
|
|
||||||
|
async setSubscriptionRecurring(
|
||||||
|
idempotencyKey: string,
|
||||||
|
recurring: SubscriptionRecurring,
|
||||||
|
plan?: SubscriptionPlan
|
||||||
|
) {
|
||||||
|
await this.store.setSubscriptionRecurring(idempotencyKey, recurring, plan);
|
||||||
|
await this.waitForRevalidation();
|
||||||
|
}
|
||||||
|
|
||||||
|
async waitForRevalidation(signal?: AbortSignal) {
|
||||||
|
this.revalidate();
|
||||||
|
await this.isRevalidating$.waitFor(
|
||||||
|
isRevalidating => !isRevalidating,
|
||||||
|
signal
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidate = effect(
|
||||||
|
exhaustMapWithTrailing(() => {
|
||||||
|
return fromPromise(async signal => {
|
||||||
|
const currentWorkspaceId = this.workspaceService.workspace.id;
|
||||||
|
if (!currentWorkspaceId) {
|
||||||
|
return undefined; // no subscription if no user
|
||||||
|
}
|
||||||
|
const serverConfig =
|
||||||
|
await this.serverService.server.features$.waitForNonNull(signal);
|
||||||
|
|
||||||
|
if (!serverConfig.payment) {
|
||||||
|
// No payment feature, no subscription
|
||||||
|
return {
|
||||||
|
workspaceId: currentWorkspaceId,
|
||||||
|
subscription: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { workspaceId, subscription } =
|
||||||
|
await this.store.fetchWorkspaceSubscriptions(
|
||||||
|
currentWorkspaceId,
|
||||||
|
signal
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
subscription: subscription,
|
||||||
|
};
|
||||||
|
}).pipe(
|
||||||
|
backoffRetry({
|
||||||
|
when: isNetworkError,
|
||||||
|
count: Infinity,
|
||||||
|
}),
|
||||||
|
backoffRetry({
|
||||||
|
when: isBackendError,
|
||||||
|
}),
|
||||||
|
mergeMap(data => {
|
||||||
|
if (data && data.subscription && data.workspaceId) {
|
||||||
|
this.store.setCachedWorkspaceSubscription(
|
||||||
|
data.workspaceId,
|
||||||
|
data.subscription
|
||||||
|
);
|
||||||
|
this.subscription$.next(data.subscription);
|
||||||
|
} else {
|
||||||
|
this.subscription$.next(undefined);
|
||||||
|
}
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
catchErrorInto(this.error$),
|
||||||
|
onStart(() => this.isRevalidating$.next(true)),
|
||||||
|
onComplete(() => this.isRevalidating$.next(false))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.subscription$.next(null);
|
||||||
|
this.team$.next(false);
|
||||||
|
this.isRevalidating$.next(false);
|
||||||
|
this.error$.next(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
override dispose(): void {
|
||||||
|
this.revalidate.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,9 @@ export { UserCopilotQuotaService } from './services/user-copilot-quota';
|
|||||||
export { UserFeatureService } from './services/user-feature';
|
export { UserFeatureService } from './services/user-feature';
|
||||||
export { UserQuotaService } from './services/user-quota';
|
export { UserQuotaService } from './services/user-quota';
|
||||||
export { WebSocketService } from './services/websocket';
|
export { WebSocketService } from './services/websocket';
|
||||||
|
export { WorkspaceInvoicesService } from './services/workspace-invoices';
|
||||||
export { WorkspaceServerService } from './services/workspace-server';
|
export { WorkspaceServerService } from './services/workspace-server';
|
||||||
|
export { WorkspaceSubscriptionService } from './services/workspace-subscription';
|
||||||
export type { ServerConfig } from './types';
|
export type { ServerConfig } from './types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -39,6 +41,7 @@ import {
|
|||||||
GlobalState,
|
GlobalState,
|
||||||
GlobalStateService,
|
GlobalStateService,
|
||||||
WorkspaceScope,
|
WorkspaceScope,
|
||||||
|
WorkspaceService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
|
|
||||||
import { UrlService } from '../url';
|
import { UrlService } from '../url';
|
||||||
@@ -51,6 +54,8 @@ import { SubscriptionPrices } from './entities/subscription-prices';
|
|||||||
import { UserCopilotQuota } from './entities/user-copilot-quota';
|
import { UserCopilotQuota } from './entities/user-copilot-quota';
|
||||||
import { UserFeature } from './entities/user-feature';
|
import { UserFeature } from './entities/user-feature';
|
||||||
import { UserQuota } from './entities/user-quota';
|
import { UserQuota } from './entities/user-quota';
|
||||||
|
import { WorkspaceInvoices } from './entities/workspace-invoices';
|
||||||
|
import { WorkspaceSubscription } from './entities/workspace-subscription';
|
||||||
import { DefaultRawFetchProvider, RawFetchProvider } from './provider/fetch';
|
import { DefaultRawFetchProvider, RawFetchProvider } from './provider/fetch';
|
||||||
import { ValidatorProvider } from './provider/validator';
|
import { ValidatorProvider } from './provider/validator';
|
||||||
import { WebSocketAuthProvider } from './provider/websocket-auth';
|
import { WebSocketAuthProvider } from './provider/websocket-auth';
|
||||||
@@ -70,7 +75,9 @@ import { UserCopilotQuotaService } from './services/user-copilot-quota';
|
|||||||
import { UserFeatureService } from './services/user-feature';
|
import { UserFeatureService } from './services/user-feature';
|
||||||
import { UserQuotaService } from './services/user-quota';
|
import { UserQuotaService } from './services/user-quota';
|
||||||
import { WebSocketService } from './services/websocket';
|
import { WebSocketService } from './services/websocket';
|
||||||
|
import { WorkspaceInvoicesService } from './services/workspace-invoices';
|
||||||
import { WorkspaceServerService } from './services/workspace-server';
|
import { WorkspaceServerService } from './services/workspace-server';
|
||||||
|
import { WorkspaceSubscriptionService } from './services/workspace-subscription';
|
||||||
import { AuthStore } from './stores/auth';
|
import { AuthStore } from './stores/auth';
|
||||||
import { CloudDocMetaStore } from './stores/cloud-doc-meta';
|
import { CloudDocMetaStore } from './stores/cloud-doc-meta';
|
||||||
import { InvoicesStore } from './stores/invoices';
|
import { InvoicesStore } from './stores/invoices';
|
||||||
@@ -142,7 +149,16 @@ 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)
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Service } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { WorkspaceInvoices } from '../entities/workspace-invoices';
|
||||||
|
|
||||||
|
export class WorkspaceInvoicesService extends Service {
|
||||||
|
invoices = this.framework.createEntity(WorkspaceInvoices);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { type CreateCheckoutSessionInput } from '@affine/graphql';
|
||||||
|
import { Service } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { SubscriptionPrices } from '../entities/subscription-prices';
|
||||||
|
import { WorkspaceSubscription } from '../entities/workspace-subscription';
|
||||||
|
import type { SubscriptionStore } from '../stores/subscription';
|
||||||
|
|
||||||
|
export class WorkspaceSubscriptionService extends Service {
|
||||||
|
subscription = this.framework.createEntity(WorkspaceSubscription);
|
||||||
|
prices = this.framework.createEntity(SubscriptionPrices);
|
||||||
|
|
||||||
|
constructor(private readonly store: SubscriptionStore) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async createCheckoutSession(input: CreateCheckoutSessionInput) {
|
||||||
|
return await this.store.createCheckoutSession(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { invoicesQuery } from '@affine/graphql';
|
import { invoicesQuery, workspaceInvoicesQuery } from '@affine/graphql';
|
||||||
import { Store } from '@toeverything/infra';
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
import type { GraphQLService } from '../services/graphql';
|
import type { GraphQLService } from '../services/graphql';
|
||||||
@@ -21,4 +21,23 @@ export class InvoicesStore extends Store {
|
|||||||
|
|
||||||
return data.currentUser;
|
return data.currentUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchWorkspaceInvoices(
|
||||||
|
skip: number,
|
||||||
|
take: number,
|
||||||
|
workspaceId: string,
|
||||||
|
signal?: AbortSignal
|
||||||
|
) {
|
||||||
|
const data = await this.graphqlService.gql({
|
||||||
|
query: workspaceInvoicesQuery,
|
||||||
|
variables: { skip, take, workspaceId },
|
||||||
|
context: { signal },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data.workspace) {
|
||||||
|
throw new Error('No workspace');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.workspace;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
import {
|
import {
|
||||||
cancelSubscriptionMutation,
|
cancelSubscriptionMutation,
|
||||||
createCheckoutSessionMutation,
|
createCheckoutSessionMutation,
|
||||||
|
getWorkspaceSubscriptionQuery,
|
||||||
pricesQuery,
|
pricesQuery,
|
||||||
resumeSubscriptionMutation,
|
resumeSubscriptionMutation,
|
||||||
SubscriptionPlan,
|
SubscriptionPlan,
|
||||||
@@ -27,7 +28,11 @@ const getDefaultSubscriptionSuccessCallbackLink = (
|
|||||||
scheme?: string
|
scheme?: string
|
||||||
) => {
|
) => {
|
||||||
const path =
|
const path =
|
||||||
plan === SubscriptionPlan.AI ? '/ai-upgrade-success' : '/upgrade-success';
|
plan === SubscriptionPlan.Team
|
||||||
|
? '/upgrade-success/team'
|
||||||
|
: plan === SubscriptionPlan.AI
|
||||||
|
? '/ai-upgrade-success'
|
||||||
|
: '/upgrade-success';
|
||||||
const urlString = baseUrl + path;
|
const urlString = baseUrl + path;
|
||||||
const url = new URL(urlString);
|
const url = new URL(urlString);
|
||||||
if (scheme) {
|
if (scheme) {
|
||||||
@@ -64,6 +69,30 @@ export class SubscriptionStore extends Store {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fetchWorkspaceSubscriptions(
|
||||||
|
workspaceId: string,
|
||||||
|
abortSignal?: AbortSignal
|
||||||
|
) {
|
||||||
|
const data = await this.gqlService.gql({
|
||||||
|
query: getWorkspaceSubscriptionQuery,
|
||||||
|
variables: {
|
||||||
|
workspaceId,
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
signal: abortSignal,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!data.workspace) {
|
||||||
|
throw new Error('No workspace');
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
workspaceId: data.workspace.subscription?.id,
|
||||||
|
subscription: data.workspace.subscription,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async mutateResumeSubscription(
|
async mutateResumeSubscription(
|
||||||
idempotencyKey: string,
|
idempotencyKey: string,
|
||||||
plan?: SubscriptionPlan,
|
plan?: SubscriptionPlan,
|
||||||
@@ -114,6 +143,22 @@ export class SubscriptionStore extends Store {
|
|||||||
return this.globalCache.set(SUBSCRIPTION_CACHE_KEY + userId, subscriptions);
|
return this.globalCache.set(SUBSCRIPTION_CACHE_KEY + userId, subscriptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCachedWorkspaceSubscription(workspaceId: string) {
|
||||||
|
return this.globalCache.get<SubscriptionType>(
|
||||||
|
SUBSCRIPTION_CACHE_KEY + workspaceId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCachedWorkspaceSubscription(
|
||||||
|
workspaceId: string,
|
||||||
|
subscription: SubscriptionType
|
||||||
|
) {
|
||||||
|
return this.globalCache.set(
|
||||||
|
SUBSCRIPTION_CACHE_KEY + workspaceId,
|
||||||
|
subscription
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
setSubscriptionRecurring(
|
setSubscriptionRecurring(
|
||||||
idempotencyKey: string,
|
idempotencyKey: string,
|
||||||
recurring: SubscriptionRecurring,
|
recurring: SubscriptionRecurring,
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
query getWorkspaceSubscription($workspaceId: String!) {
|
||||||
|
workspace(id: $workspaceId) {
|
||||||
|
subscription {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
plan
|
||||||
|
recurring
|
||||||
|
start
|
||||||
|
end
|
||||||
|
nextBillAt
|
||||||
|
canceledAt
|
||||||
|
variant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -706,6 +706,29 @@ query getWorkspacePublicPages($workspaceId: String!) {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getWorkspaceSubscriptionQuery = {
|
||||||
|
id: 'getWorkspaceSubscriptionQuery' as const,
|
||||||
|
operationName: 'getWorkspaceSubscription',
|
||||||
|
definitionName: 'workspace',
|
||||||
|
containsFile: false,
|
||||||
|
query: `
|
||||||
|
query getWorkspaceSubscription($workspaceId: String!) {
|
||||||
|
workspace(id: $workspaceId) {
|
||||||
|
subscription {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
plan
|
||||||
|
recurring
|
||||||
|
start
|
||||||
|
end
|
||||||
|
nextBillAt
|
||||||
|
canceledAt
|
||||||
|
variant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
|
||||||
export const getWorkspaceQuery = {
|
export const getWorkspaceQuery = {
|
||||||
id: 'getWorkspaceQuery' as const,
|
id: 'getWorkspaceQuery' as const,
|
||||||
operationName: 'getWorkspace',
|
operationName: 'getWorkspace',
|
||||||
@@ -1408,6 +1431,29 @@ mutation revokeInviteLink($workspaceId: String!) {
|
|||||||
}`,
|
}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const workspaceInvoicesQuery = {
|
||||||
|
id: 'workspaceInvoicesQuery' as const,
|
||||||
|
operationName: 'workspaceInvoices',
|
||||||
|
definitionName: 'workspace',
|
||||||
|
containsFile: false,
|
||||||
|
query: `
|
||||||
|
query workspaceInvoices($take: Int!, $skip: Int!, $workspaceId: String!) {
|
||||||
|
workspace(id: $workspaceId) {
|
||||||
|
invoiceCount
|
||||||
|
invoices(take: $take, skip: $skip) {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
currency
|
||||||
|
amount
|
||||||
|
reason
|
||||||
|
lastPaymentError
|
||||||
|
link
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
};
|
||||||
|
|
||||||
export const workspaceQuotaQuery = {
|
export const workspaceQuotaQuery = {
|
||||||
id: 'workspaceQuotaQuery' as const,
|
id: 'workspaceQuotaQuery' as const,
|
||||||
operationName: 'workspaceQuota',
|
operationName: 'workspaceQuota',
|
||||||
|
|||||||
15
packages/frontend/graphql/src/graphql/workspace-invoices.gql
Normal file
15
packages/frontend/graphql/src/graphql/workspace-invoices.gql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
query workspaceInvoices($take: Int!, $skip: Int!, $workspaceId: String!) {
|
||||||
|
workspace(id: $workspaceId) {
|
||||||
|
invoiceCount
|
||||||
|
invoices(take: $take, skip: $skip) {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
currency
|
||||||
|
amount
|
||||||
|
reason
|
||||||
|
lastPaymentError
|
||||||
|
link
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2014,6 +2014,29 @@ export type GetWorkspacePublicPagesQuery = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetWorkspaceSubscriptionQueryVariables = Exact<{
|
||||||
|
workspaceId: Scalars['String']['input'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type GetWorkspaceSubscriptionQuery = {
|
||||||
|
__typename?: 'Query';
|
||||||
|
workspace: {
|
||||||
|
__typename?: 'WorkspaceType';
|
||||||
|
subscription: {
|
||||||
|
__typename?: 'SubscriptionType';
|
||||||
|
id: string | null;
|
||||||
|
status: SubscriptionStatus;
|
||||||
|
plan: SubscriptionPlan;
|
||||||
|
recurring: SubscriptionRecurring;
|
||||||
|
start: string;
|
||||||
|
end: string | null;
|
||||||
|
nextBillAt: string | null;
|
||||||
|
canceledAt: string | null;
|
||||||
|
variant: SubscriptionVariant | null;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type GetWorkspaceQueryVariables = Exact<{
|
export type GetWorkspaceQueryVariables = Exact<{
|
||||||
id: Scalars['String']['input'];
|
id: Scalars['String']['input'];
|
||||||
}>;
|
}>;
|
||||||
@@ -2627,6 +2650,31 @@ export type RevokeInviteLinkMutation = {
|
|||||||
revokeInviteLink: boolean;
|
revokeInviteLink: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WorkspaceInvoicesQueryVariables = Exact<{
|
||||||
|
take: Scalars['Int']['input'];
|
||||||
|
skip: Scalars['Int']['input'];
|
||||||
|
workspaceId: Scalars['String']['input'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type WorkspaceInvoicesQuery = {
|
||||||
|
__typename?: 'Query';
|
||||||
|
workspace: {
|
||||||
|
__typename?: 'WorkspaceType';
|
||||||
|
invoiceCount: number;
|
||||||
|
invoices: Array<{
|
||||||
|
__typename?: 'InvoiceType';
|
||||||
|
id: string | null;
|
||||||
|
status: InvoiceStatus;
|
||||||
|
currency: string;
|
||||||
|
amount: number;
|
||||||
|
reason: string;
|
||||||
|
lastPaymentError: string | null;
|
||||||
|
link: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkspaceQuotaQueryVariables = Exact<{
|
export type WorkspaceQuotaQueryVariables = Exact<{
|
||||||
id: Scalars['String']['input'];
|
id: Scalars['String']['input'];
|
||||||
}>;
|
}>;
|
||||||
@@ -2813,6 +2861,11 @@ export type Queries =
|
|||||||
variables: GetWorkspacePublicPagesQueryVariables;
|
variables: GetWorkspacePublicPagesQueryVariables;
|
||||||
response: GetWorkspacePublicPagesQuery;
|
response: GetWorkspacePublicPagesQuery;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
name: 'getWorkspaceSubscriptionQuery';
|
||||||
|
variables: GetWorkspaceSubscriptionQueryVariables;
|
||||||
|
response: GetWorkspaceSubscriptionQuery;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
name: 'getWorkspaceQuery';
|
name: 'getWorkspaceQuery';
|
||||||
variables: GetWorkspaceQueryVariables;
|
variables: GetWorkspaceQueryVariables;
|
||||||
@@ -2883,6 +2936,11 @@ export type Queries =
|
|||||||
variables: ListWorkspaceFeaturesQueryVariables;
|
variables: ListWorkspaceFeaturesQueryVariables;
|
||||||
response: ListWorkspaceFeaturesQuery;
|
response: ListWorkspaceFeaturesQuery;
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
name: 'workspaceInvoicesQuery';
|
||||||
|
variables: WorkspaceInvoicesQueryVariables;
|
||||||
|
response: WorkspaceInvoicesQuery;
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
name: 'workspaceQuotaQuery';
|
name: 'workspaceQuotaQuery';
|
||||||
variables: WorkspaceQuotaQueryVariables;
|
variables: WorkspaceQuotaQueryVariables;
|
||||||
|
|||||||
Reference in New Issue
Block a user