diff --git a/packages/frontend/admin/src/modules/about/about.tsx b/packages/frontend/admin/src/modules/about/about.tsx
index 0b1df0ede2..2ce54fcfe7 100644
--- a/packages/frontend/admin/src/modules/about/about.tsx
+++ b/packages/frontend/admin/src/modules/about/about.tsx
@@ -36,9 +36,9 @@ const links = [
label: 'Self-host Document',
},
{
- href: 'https://affine.pro/pricing',
+ href: 'https://affine.pro/pricing/?type=selfhost#table',
icon: ,
- label: 'Upgrade to Pro',
+ label: 'Upgrade to Team',
},
];
diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/index.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/index.tsx
index 1b86ba0abf..73e4cc4590 100644
--- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/index.tsx
+++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/index.tsx
@@ -8,6 +8,7 @@ import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hoo
import { useMutation } from '@affine/core/components/hooks/use-mutation';
import {
AuthService,
+ SelfhostLicenseService,
WorkspaceSubscriptionService,
} from '@affine/core/modules/cloud';
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
@@ -18,14 +19,17 @@ import {
createSelfhostCustomerPortalMutation,
SubscriptionPlan,
SubscriptionRecurring,
+ SubscriptionVariant,
} from '@affine/graphql';
import { useI18n } from '@affine/i18n';
import { FrameworkScope, useLiveData, useService } from '@toeverything/infra';
+import { useCallback, useEffect, useState } from 'react';
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';
+import { UploadLicenseModal } from './upload-license-modal';
export const WorkspaceSettingLicense = ({
onCloseSetting,
@@ -52,6 +56,7 @@ export const WorkspaceSettingLicense = ({
) : (
<>
+
>
@@ -60,6 +65,52 @@ export const WorkspaceSettingLicense = ({
);
};
+const ReplaceLicenseModal = () => {
+ const t = useI18n();
+ const selfhostLicenseService = useService(SelfhostLicenseService);
+ const license = useLiveData(selfhostLicenseService.license$);
+ const isOneTimePurchase = license?.variant === SubscriptionVariant.Onetime;
+ const permission = useService(WorkspacePermissionService).permission;
+ const isTeam = useLiveData(permission.isTeam$);
+ const [openUploadModal, setOpenUploadModal] = useState(false);
+
+ const handleClick = useCallback(() => {
+ setOpenUploadModal(true);
+ }, []);
+
+ useEffect(() => {
+ selfhostLicenseService.revalidate();
+ }, [selfhostLicenseService]);
+
+ if (!isTeam || !isOneTimePurchase) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
+
const TypeFormLink = () => {
const t = useI18n();
const workspaceSubscriptionService = useService(WorkspaceSubscriptionService);
diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/self-host-team-card.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/self-host-team-card.tsx
index 67763a8a11..a86fdb4631 100644
--- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/self-host-team-card.tsx
+++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/self-host-team-card.tsx
@@ -1,20 +1,29 @@
-import { Button, ConfirmModal, Input, notify } from '@affine/component';
+import { Button, ConfirmModal, Input, Modal, notify } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
+import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
+import { useMutation } from '@affine/core/components/hooks/use-mutation';
import {
SelfhostLicenseService,
WorkspaceSubscriptionService,
} from '@affine/core/modules/cloud';
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
+import { UrlService } from '@affine/core/modules/url';
import { WorkspaceService } from '@affine/core/modules/workspace';
import { UserFriendlyError } from '@affine/error';
+import {
+ createSelfhostCustomerPortalMutation,
+ SubscriptionVariant,
+} from '@affine/graphql';
import { Trans, useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
+import { cssVarV2 } from '@toeverything/theme/v2';
import clsx from 'clsx';
import { useCallback, useEffect, useMemo, useState } from 'react';
import * as styles from './styles.css';
+import { UploadLicenseModal } from './upload-license-modal';
export const SelfHostTeamCard = () => {
const t = useI18n();
@@ -30,11 +39,13 @@ export const SelfHostTeamCard = () => {
const isLocalWorkspace = workspace.flavour === 'local';
const [openModal, setOpenModal] = useState(false);
+ const [openUploadModal, setOpenUploadModal] = useState(false);
const [loading, setLoading] = useState(false);
const selfhostLicenseService = useService(SelfhostLicenseService);
const license = useLiveData(selfhostLicenseService.license$);
+ const isOneTimePurchase = license?.variant === SubscriptionVariant.Onetime;
- useEffect(() => {
+ const revalidate = useCallback(() => {
permission.revalidate();
workspaceQuotaService.quota.revalidate();
workspaceSubscriptionService.subscription.revalidate();
@@ -46,24 +57,44 @@ export const SelfHostTeamCard = () => {
workspaceSubscriptionService,
]);
+ useEffect(() => {
+ revalidate();
+ }, [revalidate]);
+
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.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(),
+ })}
+
+ {isOneTimePurchase ? (
+
+ }}
+ />
+
+ ) : null}
+
+ );
}
return t[
'com.affine.settings.workspace.license.self-host-team.free.description'
]({
memberCount: workspaceQuota?.humanReadable.memberLimit || '10',
});
- }, [isTeam, license, t, workspaceQuota]);
+ }, [isOneTimePurchase, isTeam, license, t, workspaceQuota]);
const handleClick = useCallback(() => {
if (isLocalWorkspace) {
confirmEnableCloud(workspace);
@@ -72,6 +103,10 @@ export const SelfHostTeamCard = () => {
setOpenModal(true);
}, [confirmEnableCloud, isLocalWorkspace, workspace]);
+ const handleOpenUploadModal = useCallback(() => {
+ setOpenUploadModal(true);
+ }, []);
+
const onActivate = useCallback(
(license: string) => {
setLoading(true);
@@ -80,8 +115,7 @@ export const SelfHostTeamCard = () => {
.then(() => {
setLoading(false);
setOpenModal(false);
- permission.revalidate();
- selfhostLicenseService.revalidate();
+ revalidate();
notify.success({
title:
t['com.affine.settings.workspace.license.activate-success'](),
@@ -99,7 +133,7 @@ export const SelfHostTeamCard = () => {
});
});
},
- [permission, selfhostLicenseService, t, workspace.id]
+ [revalidate, selfhostLicenseService, t, workspace.id]
);
const onDeactivate = useCallback(() => {
@@ -109,7 +143,7 @@ export const SelfHostTeamCard = () => {
.then(() => {
setLoading(false);
setOpenModal(false);
- permission.revalidate();
+ revalidate();
notify.success({
title:
t['com.affine.settings.workspace.license.deactivate-success'](),
@@ -126,7 +160,7 @@ export const SelfHostTeamCard = () => {
message: error.message,
});
});
- }, [permission, selfhostLicenseService, t, workspace.id]);
+ }, [revalidate, selfhostLicenseService, t, workspace.id]);
const handleConfirm = useCallback(
(license: string) => {
@@ -163,7 +197,7 @@ export const SelfHostTeamCard = () => {
]()}
- {isTeam
+ {isTeam && !isOneTimePurchase
? license?.quantity || ''
: `${workspaceQuota?.memberCount}/${workspaceQuota?.memberLimit}`}
@@ -174,13 +208,24 @@ export const SelfHostTeamCard = () => {
left: isTeam || isLocalWorkspace,
})}
>
+ {!isTeam && !isLocalWorkspace ? (
+
+ ) : null}
@@ -191,6 +236,11 @@ export const SelfHostTeamCard = () => {
isTeam={!!isTeam}
loading={loading}
onConfirm={handleConfirm}
+ isOneTimePurchase={isOneTimePurchase}
+ />
+
>
);
@@ -202,16 +252,41 @@ const ActionModal = ({
isTeam,
onConfirm,
loading,
+ isOneTimePurchase,
}: {
open: boolean;
onOpenChange: (open: boolean) => void;
isTeam: boolean;
loading: boolean;
+ isOneTimePurchase: boolean;
onConfirm: (key: string) => void;
}) => {
const t = useI18n();
const [key, setKey] = useState('');
+ const workspace = useService(WorkspaceService).workspace;
+
+ const { isMutating, trigger } = useMutation({
+ mutation: createSelfhostCustomerPortalMutation,
+ });
+ const urlService = useService(UrlService);
+
+ const update = useAsyncCallback(async () => {
+ await trigger(
+ {
+ workspaceId: workspace.id,
+ },
+ {
+ onSuccess: data => {
+ urlService.openExternal(data.createSelfhostWorkspaceCustomerPortal);
+ },
+ }
+ ).catch(e => {
+ const userFriendlyError = UserFriendlyError.fromAny(e);
+ notify.error(userFriendlyError);
+ });
+ }, [trigger, urlService, workspace.id]);
+
const handleConfirm = useCallback(() => {
onConfirm(key);
setKey('');
@@ -225,23 +300,96 @@ const ActionModal = ({
[onOpenChange]
);
+ const handleCancel = useCallback(() => {
+ setKey('');
+ onOpenChange(false);
+ }, [onOpenChange]);
+
+ if (isTeam && isOneTimePurchase) {
+ return (
+
+ );
+ }
+
+ if (isTeam) {
+ return (
+ ,
+ }}
+ />
+ }
+ >
+
+
+
+
+
+
+
+
+ );
+ }
+
return (
- {isTeam ? null : (
- <>
-
-
-
- customer support
-
- ),
- 2: (
-
- click to purchase
-
- ),
- }}
- />
-
- >
- )}
+
+
+
+ ),
+ }}
+ />
+
);
};
diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/styles.css.ts b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/styles.css.ts
index 2b74f3e4a2..4e35554901 100644
--- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/styles.css.ts
+++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/styles.css.ts
@@ -53,6 +53,10 @@ export const buttonContainer = style({
export const activeButton = style({
marginTop: '8px',
});
+export const uploadButton = style({
+ marginTop: '8px',
+ marginRight: '9px',
+});
export const seat = style({
fontSize: cssVar('fontXs'),
@@ -72,3 +76,17 @@ export const tips = style({
color: cssVarV2('text/secondary'),
fontSize: cssVar('fontSm'),
});
+
+export const footer = style({
+ marginTop: 'auto',
+ marginBottom: '0',
+ display: 'flex',
+ alignItems: 'center',
+ paddingTop: '20px',
+ justifyContent: 'space-between',
+});
+
+export const rightActions = style({
+ display: 'flex',
+ gap: '20px',
+});
diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/upload-license-modal.css.ts b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/upload-license-modal.css.ts
new file mode 100644
index 0000000000..38aad66c55
--- /dev/null
+++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/upload-license-modal.css.ts
@@ -0,0 +1,109 @@
+import { cssVar } from '@toeverything/theme';
+import { cssVarV2 } from '@toeverything/theme/v2';
+import { style } from '@vanilla-extract/css';
+
+export const activateModalContent = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '12px',
+ marginTop: '12px',
+});
+
+export const tipsContainer = style({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '8px',
+ padding: '8px',
+ color: cssVarV2('text/secondary'),
+ fontSize: cssVar('fontSm'),
+ borderRadius: '8px',
+ backgroundColor: cssVarV2('layer/background/tertiary'),
+});
+
+export const tipsTitle = style({
+ fontWeight: 500,
+});
+
+export const tipsContent = style({
+ fontSize: cssVar('fontSm'),
+});
+export const textLink = style({
+ color: cssVarV2('text/link'),
+ selectors: {
+ '&:visited': {
+ color: cssVarV2('text/link'),
+ },
+ },
+});
+
+export const workspaceIdContainer = style({
+ display: 'flex',
+ justifyContent: 'space-between',
+ fontSize: cssVar('fontXs'),
+ alignItems: 'center',
+ '@media': {
+ 'screen and (max-width: 768px)': {
+ flexWrap: 'wrap',
+ },
+ },
+});
+
+export const workspaceIdLabel = style({
+ fontWeight: 500,
+});
+
+export const copyButton = style({
+ display: 'flex',
+ alignItems: 'center',
+ gap: '4px',
+ cursor: 'pointer',
+ padding: '4px 12px',
+ borderRadius: '8px',
+ width: '100%',
+ maxWidth: '300px',
+});
+export const copyButtonContent = style({
+ display: 'flex',
+ alignItems: 'center',
+ gap: '4px',
+});
+export const copyButtonText = style({
+ fontSize: cssVar('fontXs'),
+ padding: '0 4px',
+ color: cssVarV2('text/secondary'),
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap',
+ maxWidth: '100%',
+ flex: 1,
+});
+
+export const copyIcon = style({
+ width: '16px',
+ height: '16px',
+ color: cssVarV2('icon/primary'),
+});
+
+export const uploadButton = style({
+ width: '100%',
+});
+
+export const uploadButtonContent = style({
+ display: 'flex',
+ alignItems: 'center',
+ gap: '8px',
+ justifyContent: 'center',
+});
+
+export const uploadButtonIcon = style({
+ width: '16px',
+ height: '16px',
+ color: cssVarV2('layer/pureWhite'),
+});
+
+export const footer = style({
+ display: 'flex',
+ fontSize: cssVar('fontSm'),
+ fontWeight: 400,
+ color: cssVarV2('text/secondary'),
+});
diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/upload-license-modal.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/upload-license-modal.tsx
new file mode 100644
index 0000000000..ecbeb41f8c
--- /dev/null
+++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/upload-license-modal.tsx
@@ -0,0 +1,203 @@
+import { Button, Modal, notify, useConfirmModal } from '@affine/component';
+import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
+import { Upload } from '@affine/core/components/pure/file-upload';
+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 { copyTextToClipboard } from '@affine/core/utils/clipboard';
+import { UserFriendlyError } from '@affine/error';
+import { Trans, useI18n } from '@affine/i18n';
+import { CopyIcon, FileIcon } from '@blocksuite/icons/rc';
+import { useService } from '@toeverything/infra';
+import { useCallback, useEffect, useState } from 'react';
+
+import * as styles from './upload-license-modal.css';
+
+export const UploadLicenseModal = ({
+ open,
+ onOpenChange,
+}: {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}) => {
+ const t = useI18n();
+ const workspaceService = useService(WorkspaceService);
+ const workspace = workspaceService.workspace;
+ const licenseService = useService(SelfhostLicenseService);
+ const quotaService = useService(WorkspaceQuotaService);
+ const workspaceSubscriptionService = useService(WorkspaceSubscriptionService);
+ const permission = useService(WorkspacePermissionService).permission;
+ const { openConfirmModal } = useConfirmModal();
+ const [isInstalling, setIsInstalling] = useState(false);
+
+ const revalidate = useCallback(() => {
+ permission.revalidate();
+ quotaService.quota.revalidate();
+ workspaceSubscriptionService.subscription.revalidate();
+ licenseService.revalidate();
+ }, [licenseService, permission, quotaService, workspaceSubscriptionService]);
+
+ const handleInstallLicense = useAsyncCallback(
+ async (file: File) => {
+ setIsInstalling(true);
+ try {
+ await licenseService.installLicense(workspace.id, file);
+ revalidate();
+ onOpenChange(false);
+ openConfirmModal({
+ title:
+ t[
+ 'com.affine.settings.workspace.license.self-host-team.upload-license-file.success.title'
+ ](),
+ description:
+ t[
+ 'com.affine.settings.workspace.license.self-host-team.upload-license-file.success.description'
+ ](),
+ confirmText: t['Confirm'](),
+ cancelButtonOptions: {
+ style: {
+ display: 'none',
+ },
+ },
+ confirmButtonOptions: {
+ variant: 'primary',
+ },
+ });
+ } catch (e) {
+ const err = UserFriendlyError.fromAny(e);
+ onOpenChange(false);
+ console.error(err);
+ openConfirmModal({
+ title:
+ t[
+ 'com.affine.settings.workspace.license.self-host-team.upload-license-file.failed'
+ ](),
+ description: err.message,
+ confirmText: t['Confirm'](),
+ cancelButtonOptions: {
+ style: {
+ display: 'none',
+ },
+ },
+ confirmButtonOptions: {
+ variant: 'primary',
+ },
+ });
+ }
+ setIsInstalling(false);
+ },
+ [
+ licenseService,
+ onOpenChange,
+ openConfirmModal,
+ revalidate,
+ t,
+ workspace.id,
+ ]
+ );
+
+ const handleOpenChange = useCallback(
+ (open: boolean) => {
+ onOpenChange(open);
+ },
+ [onOpenChange]
+ );
+
+ const copyWorkspaceId = useCallback(() => {
+ copyTextToClipboard(workspace.id)
+ .then(success => {
+ if (success) {
+ notify.success({ title: t['Copied link to clipboard']() });
+ }
+ })
+ .catch(err => {
+ console.error(err);
+ });
+ }, [t, workspace.id]);
+
+ useEffect(() => {
+ revalidate();
+ }, [revalidate]);
+
+ return (
+
+
+
+
+ {t[
+ 'com.affine.settings.workspace.license.self-host-team.upload-license-file.tips.title'
+ ]()}
+
+
+
+ ),
+ }}
+ />
+
+
+
+ {t[
+ 'com.affine.settings.workspace.license.self-host-team.upload-license-file.tips.workspace-id'
+ ]()}
+
+
+
+
+
+
+
+
+ {t[
+ 'com.affine.settings.workspace.license.self-host-team.upload-license-file.help'
+ ]()}
+
+
+
+ );
+};
diff --git a/packages/frontend/core/src/modules/cloud/services/selfhost-license.ts b/packages/frontend/core/src/modules/cloud/services/selfhost-license.ts
index 241eca8a15..95c01b4f68 100644
--- a/packages/frontend/core/src/modules/cloud/services/selfhost-license.ts
+++ b/packages/frontend/core/src/modules/cloud/services/selfhost-license.ts
@@ -56,6 +56,10 @@ export class SelfhostLicenseService extends Service {
return await this.store.deactivate(workspaceId);
}
+ async installLicense(workspaceId: string, licenseFile: File) {
+ return await this.store.installLicense(workspaceId, licenseFile);
+ }
+
clear() {
this.license$.next(null);
this.error$.next(null);
diff --git a/packages/frontend/core/src/modules/cloud/stores/selfhost-license.ts b/packages/frontend/core/src/modules/cloud/stores/selfhost-license.ts
index d1b40588d6..5090a31a94 100644
--- a/packages/frontend/core/src/modules/cloud/stores/selfhost-license.ts
+++ b/packages/frontend/core/src/modules/cloud/stores/selfhost-license.ts
@@ -2,6 +2,7 @@ import {
activateLicenseMutation,
deactivateLicenseMutation,
getLicenseQuery,
+ installLicenseMutation,
} from '@affine/graphql';
import { Store } from '@toeverything/infra';
@@ -63,4 +64,26 @@ export class SelfhostLicenseStore extends Store {
return data.deactivateLicense;
}
+
+ async installLicense(
+ workspaceId: string,
+ license: File,
+ signal?: AbortSignal
+ ) {
+ if (!this.workspaceServerService.server) {
+ throw new Error('No Server');
+ }
+ const data = await this.workspaceServerService.server.gql({
+ query: installLicenseMutation,
+ variables: {
+ workspaceId: workspaceId,
+ license: license,
+ },
+ context: {
+ signal,
+ },
+ });
+
+ return data.installLicense;
+ }
}
diff --git a/packages/frontend/core/src/modules/quota/views/quota-check.tsx b/packages/frontend/core/src/modules/quota/views/quota-check.tsx
index 89aba6366c..3ca15886c5 100644
--- a/packages/frontend/core/src/modules/quota/views/quota-check.tsx
+++ b/packages/frontend/core/src/modules/quota/views/quota-check.tsx
@@ -37,17 +37,27 @@ export const QuotaCheck = ({
const isOwner = profile?.isOwner;
const isTeam = profile?.isTeam;
const workspaceDialogService = useService(WorkspaceDialogService);
+ const dialog = useLiveData(workspaceDialogService.dialogs$);
const t = useI18n();
const onConfirm = useCallback(() => {
if (!isOwner) {
return;
}
+ if (
+ dialog.some(
+ d =>
+ (d.type === 'setting' && d.props.activeTab === 'plans') ||
+ (d.type === 'setting' && d.props.activeTab === 'workspace:license')
+ )
+ ) {
+ return;
+ }
workspaceDialogService.open('setting', {
activeTab: 'plans',
scrollAnchor: 'cloudPricingPlan',
});
- }, [workspaceDialogService, isOwner]);
+ }, [dialog, isOwner, workspaceDialogService]);
useEffect(() => {
workspaceQuota?.revalidate();
diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts
index 0dc2db5e77..c02a1f3176 100644
--- a/packages/frontend/i18n/src/i18n.gen.ts
+++ b/packages/frontend/i18n/src/i18n.gen.ts
@@ -4454,7 +4454,7 @@ export function useAFFiNEI18N(): {
*/
["com.affine.payment.license-success.text-1"](): string;
/**
- * `You can use this key to upgrade in Settings > Workspace > Billing > Upgrade`
+ * `You can use this key to upgrade in Settings > Workspace > License > Use purchased key`
*/
["com.affine.payment.license-success.hint"](): string;
/**
@@ -6025,13 +6025,61 @@ export function useAFFiNEI18N(): {
*/
["com.affine.settings.workspace.license.self-host-team.seats"](): string;
/**
- * `Active key`
+ * `Use purchased key`
*/
- ["com.affine.settings.workspace.license.self-host-team.active-key"](): string;
+ ["com.affine.settings.workspace.license.self-host-team.use-purchased-key"](): string;
+ /**
+ * `Upload license file`
+ */
+ ["com.affine.settings.workspace.license.self-host-team.upload-license-file"](): string;
+ /**
+ * `Upload license file locally and verify the license information.`
+ */
+ ["com.affine.settings.workspace.license.self-host-team.upload-license-file.description"](): string;
+ /**
+ * `To purchase a license:`
+ */
+ ["com.affine.settings.workspace.license.self-host-team.upload-license-file.tips.title"](): string;
+ /**
+ * `Workspace id`
+ */
+ ["com.affine.settings.workspace.license.self-host-team.upload-license-file.tips.workspace-id"](): string;
+ /**
+ * `Click to upload`
+ */
+ ["com.affine.settings.workspace.license.self-host-team.upload-license-file.click-to-upload"](): string;
+ /**
+ * `Activation failed`
+ */
+ ["com.affine.settings.workspace.license.self-host-team.upload-license-file.failed"](): string;
+ /**
+ * `Activation Success`
+ */
+ ["com.affine.settings.workspace.license.self-host-team.upload-license-file.success.title"](): string;
+ /**
+ * `License has been successfully applied`
+ */
+ ["com.affine.settings.workspace.license.self-host-team.upload-license-file.success.description"](): string;
+ /**
+ * `If you encounter any issues, contact support@toeverything.info.`
+ */
+ ["com.affine.settings.workspace.license.self-host-team.upload-license-file.help"](): string;
/**
* `Deactivate`
*/
["com.affine.settings.workspace.license.self-host-team.deactivate-license"](): string;
+ /**
+ * `Replace your license file`
+ */
+ ["com.affine.settings.workspace.license.self-host-team.replace-license.title"](): string;
+ /**
+ * `Replace the existing license file with a new, updated version.`
+ */
+ ["com.affine.settings.workspace.license.self-host-team.replace-license.description"](): string;
+ /**
+ * `Upload license file`
+ */
+ ["com.affine.settings.workspace.license.self-host-team.replace-license.upload"](): string;
/**
* `Buy more seat`
*/
@@ -6049,13 +6097,17 @@ export function useAFFiNEI18N(): {
*/
["com.affine.settings.workspace.license.activate-success"](): string;
/**
- * `Deactivate License`
+ * `Confirm deactivation?`
*/
["com.affine.settings.workspace.license.deactivate-modal.title"](): string;
/**
- * `Are you sure you want to deactivate this license?`
+ * `After deactivation, you will need to upload a new license to continue using team feature`
*/
- ["com.affine.settings.workspace.license.deactivate-modal.description"](): string;
+ ["com.affine.settings.workspace.license.deactivate-modal.description-license"](): string;
+ /**
+ * `Manage Payment`
+ */
+ ["com.affine.settings.workspace.license.deactivate-modal.manage-payment"](): string;
/**
* `License deactivated successfully.`
*/
@@ -9123,11 +9175,28 @@ export const TypedTrans: {
["1"]: JSX.Element;
}>>;
/**
- * `If you encounter any issues, please contact our <1>customer support1>. No license yet? <2>Click to purchase2>.`
+ * `Activate using the local key from <1>Toeverything.Inc1>`
+ */
+ ["com.affine.settings.workspace.license.self-host-team.team.license"]: ComponentType, {
+ ["1"]: JSX.Element;
+ }>>;
+ /**
+ * `Copy your workspace id and <1>reach out to us1>.`
+ */
+ ["com.affine.settings.workspace.license.self-host-team.upload-license-file.tips.content"]: ComponentType, {
+ ["1"]: JSX.Element;
+ }>>;
+ /**
+ * `If you encounter any issues, contact support@toeverything.info. No license yet? <1>Click to purchase1>.`
*/
["com.affine.settings.workspace.license.activate-modal.tips"]: ComponentType, {
["1"]: JSX.Element;
- ["2"]: JSX.Element;
+ }>>;
+ /**
+ * `This will make the workspace read-only. Your key remains usable elsewhere. Deactivation doesn't cancel your Team plan. To cancel, go to <1>Manage Payment1>.`
+ */
+ ["com.affine.settings.workspace.license.deactivate-modal.description"]: ComponentType, {
+ ["1"]: JSX.Element;
}>>;
/**
* `The "<1>{{ name }}1>" property will be removed. This action cannot be undone.`
diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json
index 6b76b76cfa..49d84ed98a 100644
--- a/packages/frontend/i18n/src/resources/en.json
+++ b/packages/frontend/i18n/src/resources/en.json
@@ -1103,7 +1103,7 @@
"com.affine.payment.license-success.title": "Thank you for your purchase!",
"com.affine.payment.license-success.text-1": "Thank you for purchasing the AFFiNE self-hosted license.",
"com.affine.payment.license-success.text-2": "If you have any questions, please contact our <1>customer support1>.",
- "com.affine.payment.license-success.hint": "You can use this key to upgrade in Settings > Workspace > Billing > Upgrade",
+ "com.affine.payment.license-success.hint": "You can use this key to upgrade in Settings > Workspace > License > Use purchased key",
"com.affine.payment.license-success.open-affine": "Open AFFiNE",
"com.affine.payment.license-success.copy": "Copied key to clipboard",
"com.affine.peek-view-controls.close": "Close",
@@ -1498,17 +1498,33 @@
"com.affine.settings.workspace.license.self-host": "Selfhosted workspace",
"com.affine.settings.workspace.license.self-host-team": "Self-host Team Workspace",
"com.affine.settings.workspace.license.self-host-team.team.description": "This license will expire on {{expirationDate}}, with {{leftDays}} days remaining.",
+ "com.affine.settings.workspace.license.self-host-team.team.license": "Activate using the local key from <1>Toeverything.Inc1>",
"com.affine.settings.workspace.license.self-host-team.free.description": "Basic version: {{memberCount}} seats. For more, purchase or use activation key.",
"com.affine.settings.workspace.license.self-host-team.seats": "Seats",
- "com.affine.settings.workspace.license.self-host-team.active-key": "Active key",
+ "com.affine.settings.workspace.license.self-host-team.use-purchased-key": "Use purchased key",
+ "com.affine.settings.workspace.license.self-host-team.upload-license-file": "Upload license file",
+ "com.affine.settings.workspace.license.self-host-team.upload-license-file.description": "Upload license file locally and verify the license information.",
+ "com.affine.settings.workspace.license.self-host-team.upload-license-file.tips.title": "To purchase a license:",
+ "com.affine.settings.workspace.license.self-host-team.upload-license-file.tips.content": "Copy your workspace id and <1>reach out to us1>.",
+ "com.affine.settings.workspace.license.self-host-team.upload-license-file.tips.workspace-id": "Workspace id",
+ "com.affine.settings.workspace.license.self-host-team.upload-license-file.click-to-upload": "Click to upload",
+ "com.affine.settings.workspace.license.self-host-team.upload-license-file.failed": "Activation failed",
+ "com.affine.settings.workspace.license.self-host-team.upload-license-file.success.title": "Activation Success",
+ "com.affine.settings.workspace.license.self-host-team.upload-license-file.success.description": "License has been successfully applied",
+ "com.affine.settings.workspace.license.self-host-team.upload-license-file.help": "If you encounter any issues, contact support@toeverything.info.",
"com.affine.settings.workspace.license.self-host-team.deactivate-license": "Deactivate",
+ "com.affine.settings.workspace.license.self-host-team.replace-license.title": "Replace your license file",
+ "com.affine.settings.workspace.license.self-host-team.replace-license.description": "Replace the existing license file with a new, updated version.",
+ "com.affine.settings.workspace.license.self-host-team.replace-license.upload": "Upload license file",
"com.affine.settings.workspace.license.buy-more-seat": "Buy more seat",
"com.affine.settings.workspace.license.activate-modal.title": "Activate License",
"com.affine.settings.workspace.license.activate-modal.description": "Enter license key to activate this self host workspace.",
- "com.affine.settings.workspace.license.activate-modal.tips": "If you encounter any issues, please contact our <1>customer support1>. No license yet? <2>Click to purchase2>.",
+ "com.affine.settings.workspace.license.activate-modal.tips": "If you encounter any issues, contact support@toeverything.info. No license yet? <1>Click to purchase1>.",
"com.affine.settings.workspace.license.activate-success": "License activated successfully.",
- "com.affine.settings.workspace.license.deactivate-modal.title": "Deactivate License",
- "com.affine.settings.workspace.license.deactivate-modal.description": "Are you sure you want to deactivate this license?",
+ "com.affine.settings.workspace.license.deactivate-modal.title": "Confirm deactivation?",
+ "com.affine.settings.workspace.license.deactivate-modal.description": "This will make the workspace read-only. Your key remains usable elsewhere. Deactivation doesn't cancel your Team plan. To cancel, go to <1>Manage Payment1>.",
+ "com.affine.settings.workspace.license.deactivate-modal.description-license": "After deactivation, you will need to upload a new license to continue using team feature",
+ "com.affine.settings.workspace.license.deactivate-modal.manage-payment": "Manage Payment",
"com.affine.settings.workspace.license.deactivate-success": "License deactivated successfully.",
"com.affine.settings.workspace.state.local": "Local",
"com.affine.settings.workspace.state.sync-affine-cloud": "Sync with AFFiNE Cloud",
diff --git a/tools/@types/build-config/__all.d.ts b/tools/@types/build-config/__all.d.ts
index 725a72f6ea..745893bbd6 100644
--- a/tools/@types/build-config/__all.d.ts
+++ b/tools/@types/build-config/__all.d.ts
@@ -30,6 +30,7 @@ declare interface BUILD_CONFIG_TYPE {
pricingUrl: string;
downloadUrl: string;
discordUrl: string;
+ requestLicenseUrl: string;
// see: tools/workers
imageProxyUrl: string;
linkPreviewUrl: string;
diff --git a/tools/utils/src/build-config.ts b/tools/utils/src/build-config.ts
index 52cfd23347..9f6c260718 100644
--- a/tools/utils/src/build-config.ts
+++ b/tools/utils/src/build-config.ts
@@ -48,6 +48,7 @@ export function getBuildConfig(
downloadUrl: 'https://affine.pro/download',
pricingUrl: 'https://affine.pro/pricing',
discordUrl: 'https://affine.pro/redirect/discord',
+ requestLicenseUrl: 'https://affine.pro/redirect/license',
imageProxyUrl: '/api/worker/image-proxy',
linkPreviewUrl: '/api/worker/link-preview',
CAPTCHA_SITE_KEY: process.env.CAPTCHA_SITE_KEY ?? '',