mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-16 13:57:02 +08:00
feat(core): add self host team plan (#9569)
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { ConfirmModal } from '@affine/component/ui/modal';
|
||||
import { openQuotaModalAtom } from '@affine/core/components/atoms';
|
||||
import { UserQuotaService } from '@affine/core/modules/cloud';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
||||
@@ -30,18 +29,6 @@ export const CloudQuotaModal = () => {
|
||||
permissionService.permission.revalidate();
|
||||
}, [permissionService]);
|
||||
|
||||
const quotaService = useService(UserQuotaService);
|
||||
const userQuota = useLiveData(
|
||||
quotaService.quota.quota$.map(q =>
|
||||
q
|
||||
? {
|
||||
name: q.humanReadable.name,
|
||||
blobLimit: q.humanReadable.blobLimit,
|
||||
}
|
||||
: null
|
||||
)
|
||||
);
|
||||
|
||||
const workspaceDialogService = useService(WorkspaceDialogService);
|
||||
const handleUpgradeConfirm = useCallback(() => {
|
||||
workspaceDialogService.open('setting', {
|
||||
@@ -54,18 +41,19 @@ export const CloudQuotaModal = () => {
|
||||
}, [workspaceDialogService, setOpen]);
|
||||
|
||||
const description = useMemo(() => {
|
||||
if (userQuota && isOwner) {
|
||||
return <OwnerDescription quota={userQuota.blobLimit} />;
|
||||
}
|
||||
if (workspaceQuota) {
|
||||
return t['com.affine.payment.blob-limit.description.member']({
|
||||
quota: workspaceQuota.humanReadable.blobLimit,
|
||||
});
|
||||
} else {
|
||||
// loading
|
||||
if (!workspaceQuota) {
|
||||
return null;
|
||||
}
|
||||
}, [userQuota, isOwner, workspaceQuota, t]);
|
||||
if (isOwner) {
|
||||
return (
|
||||
<OwnerDescription quota={workspaceQuota.humanReadable.blobLimit} />
|
||||
);
|
||||
}
|
||||
|
||||
return t['com.affine.payment.blob-limit.description.member']({
|
||||
quota: workspaceQuota.humanReadable.blobLimit,
|
||||
});
|
||||
}, [isOwner, workspaceQuota, t]);
|
||||
|
||||
const onAbortLargeBlob = useAsyncCallback(
|
||||
async (byteSize: number) => {
|
||||
|
||||
@@ -49,15 +49,21 @@ export const generateSubscriptionCallbackLink = (
|
||||
recurring: SubscriptionRecurring,
|
||||
workspaceId?: string
|
||||
) => {
|
||||
if (account === null) {
|
||||
throw new Error('Account is required');
|
||||
}
|
||||
const baseUrl =
|
||||
plan === SubscriptionPlan.AI
|
||||
? '/ai-upgrade-success'
|
||||
: plan === SubscriptionPlan.Team
|
||||
? '/upgrade-success/team'
|
||||
: '/upgrade-success';
|
||||
: plan === SubscriptionPlan.SelfHostedTeam
|
||||
? '/upgrade-success/self-hosted-team'
|
||||
: '/upgrade-success';
|
||||
|
||||
if (plan === SubscriptionPlan.SelfHostedTeam) {
|
||||
return baseUrl;
|
||||
}
|
||||
if (account === null) {
|
||||
throw new Error('Account is required');
|
||||
}
|
||||
|
||||
let name = account?.info?.name ?? '';
|
||||
if (name.includes(separator)) {
|
||||
|
||||
@@ -130,6 +130,7 @@ const ActionButton = ({ detail, recurring }: PlanCardProps) => {
|
||||
// if contact => 'Contact Sales'
|
||||
// if not signed in:
|
||||
// if free => 'Sign up free'
|
||||
// if team => 'Upgrade'
|
||||
// else => 'Buy Pro'
|
||||
// else
|
||||
// if team => 'Start 14-day free trial'
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useWorkspaceInfo } from '@affine/core/components/hooks/use-workspace-info';
|
||||
import { ServerService } from '@affine/core/modules/cloud';
|
||||
import type { SettingTab } from '@affine/core/modules/dialogs/constant';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { ServerDeploymentType } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
CollaborationIcon,
|
||||
@@ -9,11 +11,12 @@ import {
|
||||
SaveIcon,
|
||||
SettingsIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import type { SettingSidebarItem, SettingState } from '../types';
|
||||
import { WorkspaceSettingBilling } from './billing';
|
||||
import { WorkspaceSettingLicense } from './license';
|
||||
import { MembersPanel } from './members';
|
||||
import { WorkspaceSettingDetail } from './preference';
|
||||
import { WorkspaceSettingProperties } from './properties';
|
||||
@@ -44,6 +47,8 @@ export const WorkspaceSetting = ({
|
||||
return <WorkspaceSettingBilling />;
|
||||
case 'workspace:storage':
|
||||
return <WorkspaceSettingStorage onCloseSetting={onCloseSetting} />;
|
||||
case 'workspace:license':
|
||||
return <WorkspaceSettingLicense onCloseSetting={onCloseSetting} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -52,10 +57,19 @@ export const WorkspaceSetting = ({
|
||||
export const useWorkspaceSettingList = (): SettingSidebarItem[] => {
|
||||
const workspaceService = useService(WorkspaceService);
|
||||
const information = useWorkspaceInfo(workspaceService.workspace);
|
||||
const serverService = useService(ServerService);
|
||||
|
||||
const isSelfhosted = useLiveData(
|
||||
serverService.server.config$.selector(
|
||||
c => c.type === ServerDeploymentType.Selfhosted
|
||||
)
|
||||
);
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const showBilling = information?.isTeam && information?.isOwner;
|
||||
const showBilling =
|
||||
!isSelfhosted && information?.isTeam && information?.isOwner;
|
||||
const showLicense = information?.isOwner && isSelfhosted;
|
||||
const items = useMemo<SettingSidebarItem[]>(() => {
|
||||
return [
|
||||
{
|
||||
@@ -88,10 +102,14 @@ export const useWorkspaceSettingList = (): SettingSidebarItem[] => {
|
||||
icon: <PaymentIcon />,
|
||||
testId: 'workspace-setting:billing',
|
||||
},
|
||||
|
||||
// todo(@pengx17): add selfhost's team license
|
||||
showLicense && {
|
||||
key: 'workspace:license' as SettingTab,
|
||||
title: t['com.affine.settings.workspace.license'](),
|
||||
icon: <PaymentIcon />,
|
||||
testId: 'workspace-setting:license',
|
||||
},
|
||||
].filter((item): item is SettingSidebarItem => !!item);
|
||||
}, [showBilling, t]);
|
||||
}, [showBilling, showLicense, t]);
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { Button } from '@affine/component';
|
||||
import {
|
||||
SettingHeader,
|
||||
SettingRow,
|
||||
} from '@affine/component/setting-components';
|
||||
import { getUpgradeQuestionnaireLink } from '@affine/core/components/hooks/affine/use-subscription-notify';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { useMutation } from '@affine/core/components/hooks/use-mutation';
|
||||
import {
|
||||
AuthService,
|
||||
WorkspaceSubscriptionService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { UrlService } from '@affine/core/modules/url';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import {
|
||||
createSelfhostCustomerPortalMutation,
|
||||
SubscriptionPlan,
|
||||
SubscriptionRecurring,
|
||||
} from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { FrameworkScope, useLiveData, useService } from '@toeverything/infra';
|
||||
|
||||
import { EnableCloudPanel } from '../preference/enable-cloud';
|
||||
import { SelfHostTeamCard } from './self-host-team-card';
|
||||
import { SelfHostTeamPlan } from './self-host-team-plan';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const WorkspaceSettingLicense = ({
|
||||
onCloseSetting,
|
||||
}: {
|
||||
onCloseSetting: () => void;
|
||||
}) => {
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
if (workspace === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<SettingHeader
|
||||
title={t['com.affine.settings.workspace.license']()}
|
||||
subtitle={t['com.affine.settings.workspace.license.description']()}
|
||||
/>
|
||||
<SelfHostTeamPlan />
|
||||
{workspace.flavour === 'local' ? (
|
||||
<EnableCloudPanel onCloseSetting={onCloseSetting} />
|
||||
) : (
|
||||
<>
|
||||
<SelfHostTeamCard />
|
||||
<TypeFormLink />
|
||||
<PaymentMethodUpdater />
|
||||
</>
|
||||
)}
|
||||
</FrameworkScope>
|
||||
);
|
||||
};
|
||||
|
||||
const TypeFormLink = () => {
|
||||
const t = useI18n();
|
||||
const workspaceSubscriptionService = useService(WorkspaceSubscriptionService);
|
||||
const authService = useService(AuthService);
|
||||
|
||||
const workspaceSubscription = useLiveData(
|
||||
workspaceSubscriptionService.subscription.subscription$
|
||||
);
|
||||
const account = useLiveData(authService.session.account$);
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
const link = getUpgradeQuestionnaireLink({
|
||||
name: account.info?.name,
|
||||
id: account.id,
|
||||
email: account.email,
|
||||
recurring: workspaceSubscription?.recurring ?? SubscriptionRecurring.Yearly,
|
||||
plan: SubscriptionPlan.SelfHostedTeam,
|
||||
});
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
className={styles.paymentMethod}
|
||||
name={t['com.affine.payment.billing-type-form.title']()}
|
||||
desc={t['com.affine.payment.billing-type-form.description']()}
|
||||
>
|
||||
<a target="_blank" href={link} rel="noreferrer">
|
||||
<Button>{t['com.affine.payment.billing-type-form.go']()}</Button>
|
||||
</a>
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
|
||||
const PaymentMethodUpdater = () => {
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
|
||||
const permission = useService(WorkspacePermissionService).permission;
|
||||
const isTeam = useLiveData(permission.isTeam$);
|
||||
|
||||
const { isMutating, trigger } = useMutation({
|
||||
mutation: createSelfhostCustomerPortalMutation,
|
||||
});
|
||||
const urlService = useService(UrlService);
|
||||
const t = useI18n();
|
||||
|
||||
const update = useAsyncCallback(async () => {
|
||||
await trigger(
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
{
|
||||
onSuccess: data => {
|
||||
urlService.openPopupWindow(
|
||||
data.createSelfhostWorkspaceCustomerPortal
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
}, [trigger, urlService, workspace.id]);
|
||||
|
||||
if (!isTeam) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingRow
|
||||
className={styles.paymentMethod}
|
||||
name={t['com.affine.payment.billing-setting.payment-method']()}
|
||||
desc={t[
|
||||
'com.affine.payment.billing-setting.payment-method.description'
|
||||
]()}
|
||||
>
|
||||
<Button onClick={update} loading={isMutating} disabled={isMutating}>
|
||||
{t['com.affine.payment.billing-setting.payment-method.go']()}
|
||||
</Button>
|
||||
</SettingRow>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,280 @@
|
||||
import { Button, ConfirmModal, Input, notify } from '@affine/component';
|
||||
import { SettingRow } from '@affine/component/setting-components';
|
||||
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
||||
import {
|
||||
SelfhostLicenseService,
|
||||
WorkspaceSubscriptionService,
|
||||
} from '@affine/core/modules/cloud';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { UserFriendlyError } from '@affine/graphql';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import * as styles from './styles.css';
|
||||
|
||||
export const SelfHostTeamCard = () => {
|
||||
const t = useI18n();
|
||||
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const workspaceQuotaService = useService(WorkspaceQuotaService);
|
||||
const workspaceSubscriptionService = useService(WorkspaceSubscriptionService);
|
||||
|
||||
const permission = useService(WorkspacePermissionService).permission;
|
||||
const isTeam = useLiveData(permission.isTeam$);
|
||||
const workspaceQuota = useLiveData(workspaceQuotaService.quota.quota$);
|
||||
const confirmEnableCloud = useEnableCloud();
|
||||
const isLocalWorkspace = workspace.flavour === 'local';
|
||||
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const selfhostLicenseService = useService(SelfhostLicenseService);
|
||||
const license = useLiveData(selfhostLicenseService.license$);
|
||||
|
||||
useEffect(() => {
|
||||
permission.revalidate();
|
||||
workspaceQuotaService.quota.revalidate();
|
||||
workspaceSubscriptionService.subscription.revalidate();
|
||||
selfhostLicenseService.revalidate();
|
||||
}, [
|
||||
permission,
|
||||
selfhostLicenseService,
|
||||
workspaceQuotaService,
|
||||
workspaceSubscriptionService,
|
||||
]);
|
||||
|
||||
const description = useMemo(() => {
|
||||
if (isTeam) {
|
||||
return t[
|
||||
'com.affine.settings.workspace.license.self-host-team.team.description'
|
||||
]({
|
||||
expirationDate: new Date(license?.expiredAt || 0).toLocaleDateString(),
|
||||
leftDays: Math.floor(
|
||||
(new Date(license?.expiredAt || 0).getTime() - Date.now()) /
|
||||
(1000 * 60 * 60 * 24)
|
||||
).toLocaleString(),
|
||||
});
|
||||
}
|
||||
return t[
|
||||
'com.affine.settings.workspace.license.self-host-team.free.description'
|
||||
]({
|
||||
memberCount: workspaceQuota?.humanReadable.memberLimit || '10',
|
||||
});
|
||||
}, [isTeam, license, t, workspaceQuota]);
|
||||
const handleClick = useCallback(() => {
|
||||
if (isLocalWorkspace) {
|
||||
confirmEnableCloud(workspace);
|
||||
return;
|
||||
}
|
||||
setOpenModal(true);
|
||||
}, [confirmEnableCloud, isLocalWorkspace, workspace]);
|
||||
|
||||
const onActivate = useCallback(
|
||||
(license: string) => {
|
||||
setLoading(true);
|
||||
selfhostLicenseService
|
||||
.activateLicense(workspace.id, license)
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
setOpenModal(false);
|
||||
permission.revalidate();
|
||||
selfhostLicenseService.revalidate();
|
||||
notify.success({
|
||||
title:
|
||||
t['com.affine.settings.workspace.license.activate-success'](),
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
setLoading(false);
|
||||
|
||||
console.error(e);
|
||||
const error = UserFriendlyError.fromAnyError(e);
|
||||
|
||||
notify.error({
|
||||
title: error.name,
|
||||
message: error.message,
|
||||
});
|
||||
});
|
||||
},
|
||||
[permission, selfhostLicenseService, t, workspace.id]
|
||||
);
|
||||
|
||||
const onDeactivate = useCallback(() => {
|
||||
setLoading(true);
|
||||
selfhostLicenseService
|
||||
.deactivateLicense(workspace.id)
|
||||
.then(() => {
|
||||
setLoading(false);
|
||||
setOpenModal(false);
|
||||
permission.revalidate();
|
||||
notify.success({
|
||||
title:
|
||||
t['com.affine.settings.workspace.license.deactivate-success'](),
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
setLoading(false);
|
||||
|
||||
console.error(e);
|
||||
const error = UserFriendlyError.fromAnyError(e);
|
||||
|
||||
notify.error({
|
||||
title: error.name,
|
||||
message: error.message,
|
||||
});
|
||||
});
|
||||
}, [permission, selfhostLicenseService, t, workspace.id]);
|
||||
|
||||
const handleConfirm = useCallback(
|
||||
(license: string) => {
|
||||
if (isTeam) {
|
||||
onDeactivate();
|
||||
} else {
|
||||
onActivate(license);
|
||||
}
|
||||
},
|
||||
[isTeam, onActivate, onDeactivate]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.planCard}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.currentPlan}>
|
||||
<SettingRow
|
||||
spreadCol={false}
|
||||
name={t[
|
||||
`com.affine.settings.workspace.license.self-host${isTeam ? '-team' : ''}`
|
||||
]()}
|
||||
desc={description}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(styles.planPrice, {
|
||||
hidden: isLocalWorkspace,
|
||||
})}
|
||||
>
|
||||
<span className={styles.seat}>
|
||||
{t[
|
||||
'com.affine.settings.workspace.license.self-host-team.seats'
|
||||
]()}
|
||||
</span>
|
||||
<span>
|
||||
{workspaceQuota?.memberCount}
|
||||
{isTeam ? '' : `/${workspaceQuota?.memberLimit}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={clsx(styles.buttonContainer, {
|
||||
left: isTeam || isLocalWorkspace,
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
variant="primary"
|
||||
className={styles.activeButton}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{t[
|
||||
`com.affine.settings.workspace.license.self-host-team.${isTeam ? 'deactivate-license' : 'active-key'}`
|
||||
]()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ActionModal
|
||||
open={openModal}
|
||||
onOpenChange={setOpenModal}
|
||||
isTeam={!!isTeam}
|
||||
loading={loading}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ActionModal = ({
|
||||
open,
|
||||
onOpenChange,
|
||||
isTeam,
|
||||
onConfirm,
|
||||
loading,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
isTeam: boolean;
|
||||
loading: boolean;
|
||||
onConfirm: (key: string) => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const [key, setKey] = useState('');
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
onConfirm(key);
|
||||
setKey('');
|
||||
}, [key, onConfirm]);
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
setKey('');
|
||||
onOpenChange(open);
|
||||
},
|
||||
[onOpenChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
width={480}
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
title={t[
|
||||
`com.affine.settings.workspace.license.${isTeam ? 'deactivate' : 'activate'}-modal.title`
|
||||
]()}
|
||||
description={t[
|
||||
`com.affine.settings.workspace.license.${isTeam ? 'deactivate' : 'activate'}-modal.description`
|
||||
]()}
|
||||
cancelText={t['Cancel']()}
|
||||
cancelButtonOptions={{
|
||||
variant: 'secondary',
|
||||
}}
|
||||
contentOptions={{
|
||||
['data-testid' as string]: 'invite-modal',
|
||||
style: {
|
||||
padding: '20px 24px',
|
||||
},
|
||||
}}
|
||||
confirmText={t['Confirm']()}
|
||||
confirmButtonOptions={{
|
||||
loading: loading,
|
||||
variant: isTeam ? 'error' : 'primary',
|
||||
disabled: loading || (!isTeam && !key),
|
||||
}}
|
||||
onConfirm={handleConfirm}
|
||||
childrenContentClassName={styles.activateModalContent}
|
||||
>
|
||||
{isTeam ? null : (
|
||||
<>
|
||||
<Input
|
||||
value={key}
|
||||
onChange={setKey}
|
||||
placeholder="AAAA-AAAA-AAAA-AAAA-AAAA"
|
||||
/>
|
||||
<span>
|
||||
<Trans i18nKey="com.affine.settings.workspace.license.activate-modal.tips">
|
||||
If you encounter any issues, please contact our
|
||||
<a
|
||||
href="mailto:support@toeverything.info"
|
||||
style={{ color: 'var(--affine-link-color)' }}
|
||||
>
|
||||
customer support
|
||||
</a>
|
||||
.
|
||||
</Trans>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</ConfirmModal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const pricingPlan = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '12px',
|
||||
border: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
backgroundColor: cssVarV2('layer/white'),
|
||||
borderRadius: '8px',
|
||||
gap: '18px',
|
||||
marginBottom: '24px',
|
||||
});
|
||||
|
||||
export const planCardHeader = style({
|
||||
display: 'flex',
|
||||
padding: '0px 6px',
|
||||
gap: '4px',
|
||||
flexDirection: 'column',
|
||||
fontSize: cssVar('fontBase'),
|
||||
});
|
||||
|
||||
export const planCardTitle = style({
|
||||
fontWeight: 600,
|
||||
lineHeight: '24px',
|
||||
});
|
||||
|
||||
export const planCardSubtitle = style({
|
||||
color: cssVarV2('text/secondary'),
|
||||
fontWeight: 400,
|
||||
lineHeight: '24px',
|
||||
});
|
||||
|
||||
export const benefitItems = style({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: '12px',
|
||||
gridAutoRows: 'min-content',
|
||||
'@media': {
|
||||
'screen and (max-width: 768px)': {
|
||||
gridTemplateColumns: '1fr',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const benefitItem = style({
|
||||
display: 'flex',
|
||||
gap: '8px',
|
||||
fontSize: cssVar('fontXs'),
|
||||
});
|
||||
|
||||
export const doneIconStyle = style({
|
||||
color: cssVarV2('button/primary'),
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
});
|
||||
|
||||
export const leanMoreButtonContainer = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import { Button } from '@affine/component';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { DoneIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import * as styles from './self-host-team-plan.css';
|
||||
|
||||
const initialQuota = '100 GB';
|
||||
const quotaPerSeat = '20 GB';
|
||||
const maxFileSize = '500 MB';
|
||||
|
||||
export const SelfHostTeamPlan = () => {
|
||||
const t = useI18n();
|
||||
|
||||
const permission = useService(WorkspacePermissionService).permission;
|
||||
const isTeam = useLiveData(permission.isTeam$);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
window.open(BUILD_CONFIG.pricingUrl, '_blank');
|
||||
}, []);
|
||||
|
||||
if (isTeam) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.pricingPlan}>
|
||||
<div className={styles.planCardHeader}>
|
||||
<div className={styles.planCardTitle}>
|
||||
{t['com.affine.settings.workspace.license.benefit.team.title']()}
|
||||
</div>
|
||||
<div className={styles.planCardSubtitle}>
|
||||
{t['com.affine.settings.workspace.license.benefit.team.subtitle']()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.benefitItems}>
|
||||
<div className={styles.benefitItem}>
|
||||
<DoneIcon className={styles.doneIconStyle} />
|
||||
{t['com.affine.settings.workspace.license.benefit.team.g1']()}
|
||||
</div>
|
||||
<div className={styles.benefitItem}>
|
||||
<DoneIcon className={styles.doneIconStyle} />
|
||||
{t['com.affine.settings.workspace.license.benefit.team.g2']({
|
||||
initialQuota,
|
||||
quotaPerSeat,
|
||||
})}
|
||||
</div>
|
||||
<div className={styles.benefitItem}>
|
||||
<DoneIcon className={styles.doneIconStyle} />
|
||||
{t['com.affine.settings.workspace.license.benefit.team.g3']({
|
||||
quota: maxFileSize,
|
||||
})}
|
||||
</div>
|
||||
<div className={styles.benefitItem}>
|
||||
<DoneIcon className={styles.doneIconStyle} />
|
||||
{t['com.affine.settings.workspace.license.benefit.team.g4']()}
|
||||
</div>
|
||||
<div className={styles.benefitItem}>
|
||||
<DoneIcon className={styles.doneIconStyle} />
|
||||
{t['com.affine.settings.workspace.license.benefit.team.g5']()}
|
||||
</div>
|
||||
<div className={styles.benefitItem}>
|
||||
<DoneIcon className={styles.doneIconStyle} />
|
||||
{t['com.affine.settings.workspace.license.benefit.team.g6']()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.leanMoreButtonContainer}>
|
||||
<Button onClick={handleClick}>
|
||||
{t['com.affine.settings.workspace.license.lean-more']()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const paymentMethod = style({
|
||||
marginTop: '24px',
|
||||
});
|
||||
|
||||
export const planCard = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: '12px',
|
||||
border: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
backgroundColor: cssVarV2('layer/white'),
|
||||
borderRadius: '8px',
|
||||
});
|
||||
|
||||
export const container = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
});
|
||||
|
||||
export const currentPlan = style({
|
||||
flex: '1 0 0',
|
||||
});
|
||||
|
||||
export const planPrice = style({
|
||||
fontSize: cssVar('fontH6'),
|
||||
fontWeight: 600,
|
||||
display: 'flex',
|
||||
gap: '4px',
|
||||
margin: '0px 4px',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
alignItems: 'center',
|
||||
selectors: {
|
||||
'&.hidden': {
|
||||
visibility: 'hidden',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const buttonContainer = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
marginTop: '12px',
|
||||
selectors: {
|
||||
'&.left': {
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const activeButton = style({
|
||||
marginTop: '8px',
|
||||
});
|
||||
|
||||
export const seat = style({
|
||||
fontSize: cssVar('fontXs'),
|
||||
fontWeight: 400,
|
||||
});
|
||||
|
||||
export const activateModalContent = style({
|
||||
padding: '0',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '12px',
|
||||
marginTop: '12px',
|
||||
marginBottom: '20px',
|
||||
});
|
||||
|
||||
export const tips = style({
|
||||
color: cssVarV2('text/secondary'),
|
||||
fontSize: cssVar('fontSm'),
|
||||
});
|
||||
@@ -260,7 +260,7 @@ const getMemberStatus = (member: Member): I18nString => {
|
||||
return 'Workspace Owner';
|
||||
case Permission.Admin:
|
||||
return 'Admin';
|
||||
case Permission.Write:
|
||||
case Permission.Collaborator:
|
||||
return 'Collaborator';
|
||||
default:
|
||||
return 'Member';
|
||||
|
||||
@@ -141,7 +141,7 @@ export const MemberOptions = ({
|
||||
}, [member, membersService, t]);
|
||||
const handleChangeToCollaborator = useCallback(() => {
|
||||
membersService
|
||||
.adjustMemberPermission(member.id, Permission.Write)
|
||||
.adjustMemberPermission(member.id, Permission.Collaborator)
|
||||
.then(result => {
|
||||
if (result) {
|
||||
notify.success({
|
||||
|
||||
@@ -36,6 +36,8 @@ const products = {
|
||||
believer: 'pro_lifetime',
|
||||
team: 'team_yearly',
|
||||
'monthly-team': 'team_monthly',
|
||||
'yearly-selfhost-team': 'selfhost-team_yearly',
|
||||
'monthly-selfhost-team': 'selfhost-team_monthly',
|
||||
'oneyear-ai': 'ai_yearly_onetime',
|
||||
'oneyear-pro': 'pro_yearly_onetime',
|
||||
'onemonth-pro': 'pro_monthly_onetime',
|
||||
@@ -45,6 +47,7 @@ const allowedPlan = {
|
||||
ai: SubscriptionPlan.AI,
|
||||
pro: SubscriptionPlan.Pro,
|
||||
team: SubscriptionPlan.Team,
|
||||
'selfhost-team': SubscriptionPlan.SelfHostedTeam,
|
||||
};
|
||||
const allowedRecurring = {
|
||||
monthly: SubscriptionRecurring.Monthly,
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Button, IconButton, Loading, notify } from '@affine/component';
|
||||
import { AuthPageContainer } from '@affine/component/auth-components';
|
||||
import { SelfhostGenerateLicenseService } from '@affine/core/modules/cloud';
|
||||
import { OpenInAppService } from '@affine/core/modules/open-in-app';
|
||||
import { copyTextToClipboard } from '@affine/core/utils/clipboard';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import { CopyIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
import { PageNotFound } from '../../404';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
/**
|
||||
* /upgrade-success/self-hosted-team page
|
||||
*
|
||||
* only on web
|
||||
*/
|
||||
export const Component = () => {
|
||||
const [params] = useSearchParams();
|
||||
const sessionId = params.get('session_id');
|
||||
const selfhostGenerateLicenseService = useService(
|
||||
SelfhostGenerateLicenseService
|
||||
);
|
||||
const isMutating = useLiveData(selfhostGenerateLicenseService.isLoading$);
|
||||
const key = useLiveData(selfhostGenerateLicenseService.licenseKey$);
|
||||
const error = useLiveData(selfhostGenerateLicenseService.error$);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMutating || error) {
|
||||
return;
|
||||
}
|
||||
if (sessionId && !key) {
|
||||
selfhostGenerateLicenseService.generateLicenseKey(sessionId);
|
||||
}
|
||||
}, [error, isMutating, key, selfhostGenerateLicenseService, sessionId]);
|
||||
|
||||
if (!sessionId) {
|
||||
return <PageNotFound noPermission />;
|
||||
}
|
||||
if (isMutating || key) {
|
||||
return <Success licenseKey={key} />;
|
||||
} else {
|
||||
return (
|
||||
<AuthPageContainer
|
||||
title={'failed to generate license key'}
|
||||
subtitle={error?.message}
|
||||
></AuthPageContainer>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const Success = ({ licenseKey }: { licenseKey: string | null }) => {
|
||||
const t = useI18n();
|
||||
const openInAppService = useService(OpenInAppService);
|
||||
|
||||
const openAFFiNE = useCallback(() => {
|
||||
openInAppService.showOpenInAppPage();
|
||||
}, [openInAppService]);
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
if (!licenseKey) {
|
||||
notify.error({ title: 'Copy failed, please try again later' });
|
||||
return;
|
||||
}
|
||||
copyTextToClipboard(licenseKey)
|
||||
.then(success => {
|
||||
if (success) {
|
||||
notify.success({
|
||||
title: t['com.affine.payment.license-success.copy'](),
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
notify.error({ title: 'Copy failed, please try again later' });
|
||||
});
|
||||
}, [licenseKey, t]);
|
||||
|
||||
const subtitle = (
|
||||
<span className={styles.leftContentText}>
|
||||
<span>{t['com.affine.payment.license-success.text-1']()}</span>
|
||||
<span>
|
||||
<Trans
|
||||
i18nKey={'com.affine.payment.license-success.text-2'}
|
||||
components={{
|
||||
1: (
|
||||
<a
|
||||
href="mailto:support@toeverything.info"
|
||||
className={styles.mail}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<AuthPageContainer
|
||||
title={t['com.affine.payment.license-success.title']()}
|
||||
subtitle={subtitle}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.licenseKeyContainer}>
|
||||
{licenseKey ? licenseKey : <Loading />}
|
||||
<IconButton
|
||||
icon={<CopyIcon />}
|
||||
className={styles.icon}
|
||||
size="20"
|
||||
tooltip={t['Copy']()}
|
||||
onClick={onCopy}
|
||||
/>
|
||||
</div>
|
||||
<div>{t['com.affine.payment.license-success.hint']()}</div>
|
||||
<div>
|
||||
<Button variant="primary" size="extraLarge" onClick={openAFFiNE}>
|
||||
{t['com.affine.payment.license-success.open-affine']()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AuthPageContainer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
export const leftContentText = style({
|
||||
fontSize: cssVar('fontBase'),
|
||||
fontWeight: 400,
|
||||
lineHeight: '1.6',
|
||||
maxWidth: '548px',
|
||||
});
|
||||
export const mail = style({
|
||||
color: cssVar('linkColor'),
|
||||
textDecoration: 'none',
|
||||
':visited': {
|
||||
color: cssVar('linkColor'),
|
||||
},
|
||||
});
|
||||
export const content = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '28px',
|
||||
});
|
||||
|
||||
export const licenseKeyContainer = style({
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
backgroundColor: cssVarV2('layer/background/secondary'),
|
||||
borderRadius: '4px',
|
||||
border: `1px solid ${cssVarV2('layer/insideBorder/blackBorder')}`,
|
||||
padding: '8px 10px',
|
||||
gap: '8px',
|
||||
});
|
||||
|
||||
export const icon = style({
|
||||
color: cssVarV2('icon/primary'),
|
||||
});
|
||||
@@ -68,6 +68,10 @@ export const topLevelRoutes = [
|
||||
path: '/upgrade-success/team',
|
||||
lazy: () => import('./pages/upgrade-success/team'),
|
||||
},
|
||||
{
|
||||
path: '/upgrade-success/self-hosted-team',
|
||||
lazy: () => import('./pages/upgrade-success/self-host-team'),
|
||||
},
|
||||
{
|
||||
path: '/ai-upgrade-success',
|
||||
lazy: () => import('./pages/ai-upgrade-success'),
|
||||
|
||||
@@ -19,6 +19,8 @@ export { EventSourceService } from './services/eventsource';
|
||||
export { FetchService } from './services/fetch';
|
||||
export { GraphQLService } from './services/graphql';
|
||||
export { InvoicesService } from './services/invoices';
|
||||
export { SelfhostGenerateLicenseService } from './services/selfhost-generate-license';
|
||||
export { SelfhostLicenseService } from './services/selfhost-license';
|
||||
export { ServerService } from './services/server';
|
||||
export { ServersService } from './services/servers';
|
||||
export { SubscriptionService } from './services/subscription';
|
||||
@@ -58,6 +60,8 @@ import { EventSourceService } from './services/eventsource';
|
||||
import { FetchService } from './services/fetch';
|
||||
import { GraphQLService } from './services/graphql';
|
||||
import { InvoicesService } from './services/invoices';
|
||||
import { SelfhostGenerateLicenseService } from './services/selfhost-generate-license';
|
||||
import { SelfhostLicenseService } from './services/selfhost-license';
|
||||
import { ServerService } from './services/server';
|
||||
import { ServersService } from './services/servers';
|
||||
import { SubscriptionService } from './services/subscription';
|
||||
@@ -70,6 +74,8 @@ import { WorkspaceSubscriptionService } from './services/workspace-subscription'
|
||||
import { AuthStore } from './stores/auth';
|
||||
import { CloudDocMetaStore } from './stores/cloud-doc-meta';
|
||||
import { InvoicesStore } from './stores/invoices';
|
||||
import { SelfhostGenerateLicenseStore } from './stores/selfhost-generate-license';
|
||||
import { SelfhostLicenseStore } from './stores/selfhost-license';
|
||||
import { ServerConfigStore } from './stores/server-config';
|
||||
import { ServerListStore } from './stores/server-list';
|
||||
import { SubscriptionStore } from './stores/subscription';
|
||||
@@ -128,7 +134,9 @@ export function configureCloudModule(framework: Framework) {
|
||||
.store(UserFeatureStore, [GraphQLService])
|
||||
.service(InvoicesService)
|
||||
.store(InvoicesStore, [GraphQLService])
|
||||
.entity(Invoices, [InvoicesStore]);
|
||||
.entity(Invoices, [InvoicesStore])
|
||||
.service(SelfhostGenerateLicenseService, [SelfhostGenerateLicenseStore])
|
||||
.store(SelfhostGenerateLicenseStore, [GraphQLService]);
|
||||
|
||||
framework
|
||||
.scope(WorkspaceScope)
|
||||
@@ -142,5 +150,7 @@ export function configureCloudModule(framework: Framework) {
|
||||
.service(WorkspaceSubscriptionService, [WorkspaceServerService])
|
||||
.entity(WorkspaceSubscription, [WorkspaceService, WorkspaceServerService])
|
||||
.service(WorkspaceInvoicesService)
|
||||
.entity(WorkspaceInvoices, [WorkspaceService, WorkspaceServerService]);
|
||||
.entity(WorkspaceInvoices, [WorkspaceService, WorkspaceServerService])
|
||||
.service(SelfhostLicenseService, [SelfhostLicenseStore, WorkspaceService])
|
||||
.store(SelfhostLicenseStore, [WorkspaceServerService]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { UserFriendlyError } from '@affine/graphql';
|
||||
import {
|
||||
backoffRetry,
|
||||
effect,
|
||||
fromPromise,
|
||||
LiveData,
|
||||
onComplete,
|
||||
onStart,
|
||||
Service,
|
||||
} from '@toeverything/infra';
|
||||
import { catchError, EMPTY, exhaustMap, mergeMap } from 'rxjs';
|
||||
|
||||
import { isBackendError, isNetworkError } from '../error';
|
||||
import type { SelfhostGenerateLicenseStore } from '../stores/selfhost-generate-license';
|
||||
|
||||
export class SelfhostGenerateLicenseService extends Service {
|
||||
constructor(private readonly store: SelfhostGenerateLicenseStore) {
|
||||
super();
|
||||
}
|
||||
licenseKey$ = new LiveData<string | null>(null);
|
||||
isLoading$ = new LiveData(false);
|
||||
error$ = new LiveData<UserFriendlyError | null>(null);
|
||||
|
||||
generateLicenseKey = effect(
|
||||
exhaustMap((sessionId: string) => {
|
||||
return fromPromise(async () => {
|
||||
return await this.store.generateKey(sessionId);
|
||||
}).pipe(
|
||||
backoffRetry({
|
||||
when: isNetworkError,
|
||||
count: Infinity,
|
||||
}),
|
||||
backoffRetry({
|
||||
when: isBackendError,
|
||||
}),
|
||||
mergeMap(key => {
|
||||
this.licenseKey$.next(key);
|
||||
return EMPTY;
|
||||
}),
|
||||
catchError(err => {
|
||||
this.error$.next(UserFriendlyError.fromAnyError(err));
|
||||
console.error(err);
|
||||
return EMPTY;
|
||||
}),
|
||||
onStart(() => {
|
||||
this.isLoading$.next(true);
|
||||
}),
|
||||
onComplete(() => {
|
||||
this.isLoading$.next(false);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { License } from '@affine/graphql';
|
||||
import {
|
||||
backoffRetry,
|
||||
catchErrorInto,
|
||||
effect,
|
||||
exhaustMapWithTrailing,
|
||||
fromPromise,
|
||||
LiveData,
|
||||
onComplete,
|
||||
onStart,
|
||||
Service,
|
||||
} from '@toeverything/infra';
|
||||
import { EMPTY, mergeMap } from 'rxjs';
|
||||
|
||||
import type { WorkspaceService } from '../../workspace';
|
||||
import { isBackendError, isNetworkError } from '../error';
|
||||
import type { SelfhostLicenseStore } from '../stores/selfhost-license';
|
||||
|
||||
export class SelfhostLicenseService extends Service {
|
||||
constructor(
|
||||
private readonly store: SelfhostLicenseStore,
|
||||
private readonly workspaceService: WorkspaceService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
license$ = new LiveData<License | null>(null);
|
||||
isRevalidating$ = new LiveData(false);
|
||||
error$ = new LiveData<any | null>(null);
|
||||
|
||||
revalidate = effect(
|
||||
exhaustMapWithTrailing(() => {
|
||||
return fromPromise(async signal => {
|
||||
const currentWorkspaceId = this.workspaceService.workspace.id;
|
||||
if (!currentWorkspaceId) {
|
||||
return undefined; // no subscription if no user
|
||||
}
|
||||
return await this.store.getLicense(currentWorkspaceId, signal);
|
||||
}).pipe(
|
||||
backoffRetry({
|
||||
when: isNetworkError,
|
||||
count: Infinity,
|
||||
}),
|
||||
backoffRetry({
|
||||
when: isBackendError,
|
||||
}),
|
||||
mergeMap(data => {
|
||||
if (data) {
|
||||
this.license$.next(data);
|
||||
}
|
||||
|
||||
return EMPTY;
|
||||
}),
|
||||
catchErrorInto(this.error$),
|
||||
onStart(() => this.isRevalidating$.next(true)),
|
||||
onComplete(() => this.isRevalidating$.next(false))
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
async activateLicense(workspaceId: string, licenseKey: string) {
|
||||
return await this.store.activate(workspaceId, licenseKey);
|
||||
}
|
||||
|
||||
async deactivateLicense(workspaceId: string) {
|
||||
return await this.store.deactivate(workspaceId);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.license$.next(null);
|
||||
this.error$.next(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { generateLicenseKeyMutation } from '@affine/graphql';
|
||||
import { Store } from '@toeverything/infra';
|
||||
|
||||
import type { GraphQLService } from '../services/graphql';
|
||||
|
||||
export class SelfhostGenerateLicenseStore extends Store {
|
||||
constructor(private readonly gqlService: GraphQLService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async generateKey(sessionId: string, signal?: AbortSignal): Promise<string> {
|
||||
const data = await this.gqlService.gql({
|
||||
query: generateLicenseKeyMutation,
|
||||
variables: {
|
||||
sessionId: sessionId,
|
||||
},
|
||||
context: {
|
||||
signal,
|
||||
},
|
||||
});
|
||||
|
||||
return data.generateLicenseKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import {
|
||||
activateLicenseMutation,
|
||||
deactivateLicenseMutation,
|
||||
getLicenseQuery,
|
||||
} from '@affine/graphql';
|
||||
import { Store } from '@toeverything/infra';
|
||||
|
||||
import type { WorkspaceServerService } from '../services/workspace-server';
|
||||
|
||||
export class SelfhostLicenseStore extends Store {
|
||||
constructor(private readonly workspaceServerService: WorkspaceServerService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async getLicense(workspaceId: string, signal?: AbortSignal) {
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
const data = await this.workspaceServerService.server.gql({
|
||||
query: getLicenseQuery,
|
||||
variables: {
|
||||
workspaceId: workspaceId,
|
||||
},
|
||||
context: {
|
||||
signal,
|
||||
},
|
||||
});
|
||||
|
||||
return data.workspace.license;
|
||||
}
|
||||
|
||||
async activate(workspaceId: string, license: string, signal?: AbortSignal) {
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
const data = await this.workspaceServerService.server.gql({
|
||||
query: activateLicenseMutation,
|
||||
variables: {
|
||||
workspaceId: workspaceId,
|
||||
license: license,
|
||||
},
|
||||
context: {
|
||||
signal,
|
||||
},
|
||||
});
|
||||
|
||||
return data.activateLicense;
|
||||
}
|
||||
|
||||
async deactivate(workspaceId: string, signal?: AbortSignal) {
|
||||
if (!this.workspaceServerService.server) {
|
||||
throw new Error('No Server');
|
||||
}
|
||||
const data = await this.workspaceServerService.server.gql({
|
||||
query: deactivateLicenseMutation,
|
||||
variables: {
|
||||
workspaceId: workspaceId,
|
||||
},
|
||||
context: {
|
||||
signal,
|
||||
},
|
||||
});
|
||||
|
||||
return data.deactivateLicense;
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,13 @@ import {
|
||||
catchErrorInto,
|
||||
effect,
|
||||
Entity,
|
||||
exhaustMapWithTrailing,
|
||||
fromPromise,
|
||||
LiveData,
|
||||
onComplete,
|
||||
onStart,
|
||||
} from '@toeverything/infra';
|
||||
import { EMPTY, exhaustMap, mergeMap } from 'rxjs';
|
||||
import { EMPTY, mergeMap } from 'rxjs';
|
||||
|
||||
import { isBackendError, isNetworkError } from '../../cloud';
|
||||
import type { WorkspaceService } from '../../workspace';
|
||||
@@ -32,7 +33,7 @@ export class WorkspacePermission extends Entity {
|
||||
}
|
||||
|
||||
revalidate = effect(
|
||||
exhaustMap(() => {
|
||||
exhaustMapWithTrailing(() => {
|
||||
return fromPromise(async signal => {
|
||||
if (this.workspaceService.workspace.flavour !== 'local') {
|
||||
const info = await this.store.fetchWorkspaceInfo(
|
||||
|
||||
Reference in New Issue
Block a user