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 support. No license yet? <2>Click to purchase.` + * `Activate using the local key from <1>Toeverything.Inc` + */ + ["com.affine.settings.workspace.license.self-host-team.team.license"]: ComponentType, { + ["1"]: JSX.Element; + }>>; + /** + * `Copy your workspace id and <1>reach out to us.` + */ + ["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 purchase.` */ ["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 Payment.` + */ + ["com.affine.settings.workspace.license.deactivate-modal.description"]: ComponentType, { + ["1"]: JSX.Element; }>>; /** * `The "<1>{{ name }}" 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 support.", - "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.Inc", "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 us.", + "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 support. No license yet? <2>Click to purchase.", + "com.affine.settings.workspace.license.activate-modal.tips": "If you encounter any issues, contact support@toeverything.info. No license yet? <1>Click to purchase.", "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 Payment.", + "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 ?? '',