diff --git a/packages/backend/server/src/plugins/license/service.ts b/packages/backend/server/src/plugins/license/service.ts index 5d1491224b228..f323b36adf468 100644 --- a/packages/backend/server/src/plugins/license/service.ts +++ b/packages/backend/server/src/plugins/license/service.ts @@ -55,7 +55,7 @@ export class LicenseService { throw new WorkspaceLicenseAlreadyExists(); } - const data = await this.fetch( + const data = await this.fetchAffinePro( `/api/team/licenses/${licenseKey}/activate`, { method: 'POST', @@ -105,7 +105,7 @@ export class LicenseService { throw new LicenseNotFound(); } - await this.fetch(`/api/team/licenses/${license.key}/deactivate`, { + await this.fetchAffinePro(`/api/team/licenses/${license.key}/deactivate`, { method: 'POST', }); @@ -120,10 +120,11 @@ export class LicenseService { plan: SubscriptionPlan.SelfHostedTeam, recurring: SubscriptionRecurring.Monthly, }); + return true; } async updateTeamRecurring(key: string, recurring: SubscriptionRecurring) { - await this.fetch(`/api/team/licenses/${key}/recurring`, { + await this.fetchAffinePro(`/api/team/licenses/${key}/recurring`, { method: 'POST', body: JSON.stringify({ recurring, @@ -142,7 +143,7 @@ export class LicenseService { throw new LicenseNotFound(); } - return this.fetch<{ url: string }>( + return this.fetchAffinePro<{ url: string }>( `/api/team/licenses/${license.key}/create-customer-portal`, { method: 'POST', @@ -164,7 +165,7 @@ export class LicenseService { return; } - await this.fetch(`/api/team/licenses/${license.key}/seats`, { + await this.fetchAffinePro(`/api/team/licenses/${license.key}/seats`, { method: 'POST', body: JSON.stringify({ quantity: count, @@ -218,7 +219,7 @@ export class LicenseService { private async revalidateLicense(license: InstalledLicense) { try { - const res = await this.fetch( + const res = await this.fetchAffinePro( `/api/team/licenses/${license.key}/health` ); @@ -262,7 +263,7 @@ export class LicenseService { } } - private async fetch( + private async fetchAffinePro( path: string, init?: RequestInit ): Promise { diff --git a/packages/backend/server/src/plugins/payment/resolver.ts b/packages/backend/server/src/plugins/payment/resolver.ts index 45c7344e027c1..b233f8fcc2a4c 100644 --- a/packages/backend/server/src/plugins/payment/resolver.ts +++ b/packages/backend/server/src/plugins/payment/resolver.ts @@ -278,7 +278,7 @@ export class SubscriptionResolver { if (input.plan === SubscriptionPlan.SelfHostedTeam) { session = await this.service.checkout(input, { plan: input.plan as any, - quantity: input.args.quantity ?? 10, + quantity: input.args?.quantity ?? 10, }); } else { if (!user) { diff --git a/packages/frontend/core/src/components/affine/quota-reached-modal/cloud-quota-modal.tsx b/packages/frontend/core/src/components/affine/quota-reached-modal/cloud-quota-modal.tsx index fe3005e60b2ee..8dd1149bfd159 100644 --- a/packages/frontend/core/src/components/affine/quota-reached-modal/cloud-quota-modal.tsx +++ b/packages/frontend/core/src/components/affine/quota-reached-modal/cloud-quota-modal.tsx @@ -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 ; - } - 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 ( + + ); + } + + return t['com.affine.payment.blob-limit.description.member']({ + quota: workspaceQuota.humanReadable.blobLimit, + }); + }, [isOwner, workspaceQuota, t]); const onAbortLargeBlob = useAsyncCallback( async (byteSize: number) => { diff --git a/packages/frontend/core/src/components/hooks/affine/use-subscription-notify.tsx b/packages/frontend/core/src/components/hooks/affine/use-subscription-notify.tsx index 9c48d62df267a..a8cc253eb223f 100644 --- a/packages/frontend/core/src/components/hooks/affine/use-subscription-notify.tsx +++ b/packages/frontend/core/src/components/hooks/affine/use-subscription-notify.tsx @@ -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)) { diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/cloud-plans.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/cloud-plans.tsx index db88da3939f71..b679d8f865031 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/cloud-plans.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/cloud-plans.tsx @@ -171,6 +171,40 @@ export function getPlanDetail(t: T) { benefits: teamBenefits(t), }, ], + [ + SubscriptionPlan.SelfHostedTeam, + { + type: 'fixed', + plan: SubscriptionPlan.SelfHostedTeam, + price: '12', + yearlyPrice: '10', + name: 'Self-hosted Team', + description: 'Self-hosted Team description', + titleRenderer: (recurring, detail) => { + const price = + recurring === SubscriptionRecurring.Yearly + ? detail.yearlyPrice + : detail.price; + return ( + <> + {t['com.affine.payment.cloud.team-workspace.title.price-monthly']( + { + price: '$' + price, + } + )} + {recurring === SubscriptionRecurring.Yearly ? ( + + {t[ + 'com.affine.payment.cloud.team-workspace.title.billed-yearly' + ]()} + + ) : null} + + ); + }, + benefits: teamBenefits(t), + }, + ], ]); } diff --git a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/plan-card.tsx b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/plan-card.tsx index ae65910b0faa8..518a5f6c23f50 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/plan-card.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/general-setting/plans/plan-card.tsx @@ -10,6 +10,7 @@ import { import { GlobalDialogService } from '@affine/core/modules/dialogs'; import { type CreateCheckoutSessionInput, + ServerDeploymentType, SubscriptionPlan, SubscriptionRecurring, SubscriptionStatus, @@ -121,6 +122,13 @@ const ActionButton = ({ detail, recurring }: PlanCardProps) => { const isOnetime = useLiveData(subscriptionService.subscription.isOnetimePro$); const isFree = detail.plan === SubscriptionPlan.Free; + const serverService = useService(ServerService); + const isSelfHosted = useLiveData( + serverService.server.config$.selector( + c => c.type === ServerDeploymentType.Selfhosted + ) + ); + const signUpText = useMemo( () => getSignUpText(detail.plan, t), [detail.plan, t] @@ -128,8 +136,10 @@ const ActionButton = ({ detail, recurring }: PlanCardProps) => { // branches: // if contact => 'Contact Sales' + // if self-hosted team => 'Upgrade' // if not signed in: // if free => 'Sign up free' + // if team => 'Upgrade' // else => 'Buy Pro' // else // if team => 'Start 14-day free trial' @@ -144,6 +154,13 @@ const ActionButton = ({ detail, recurring }: PlanCardProps) => { // if currentRecurring !== recurring => 'Change to {recurring} Billing' // else => 'Upgrade' + // self-hosted team + if (isSelfHosted || detail.plan === SubscriptionPlan.SelfHostedTeam) { + return ( + + ); + } + // not signed in if (!loggedIn) { return {signUpText}; @@ -267,6 +284,51 @@ const UpgradeToTeam = ({ recurring }: { recurring: SubscriptionRecurring }) => { ); }; +const UpgradeToSelfHostTeam = ({ + recurring, +}: { + recurring: SubscriptionRecurring; +}) => { + const t = useI18n(); + + const handleBeforeCheckout = useCallback(() => { + track.$.settingsPanel.plans.checkout({ + plan: SubscriptionPlan.SelfHostedTeam, + recurring: recurring, + }); + }, [recurring]); + + const checkoutOptions = useMemo( + () => ({ + recurring, + plan: SubscriptionPlan.SelfHostedTeam, + variant: null, + coupon: null, + successCallbackLink: generateSubscriptionCallbackLink( + null, + SubscriptionPlan.SelfHostedTeam, + recurring + ), + }), + [recurring] + ); + + return ( + ( + + )} + /> + ); +}; export const Upgrade = ({ className, diff --git a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/index.tsx b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/index.tsx index af722dafdc875..9e0998155542f 100644 --- a/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/index.tsx +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/index.tsx @@ -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,10 @@ export const WorkspaceSetting = ({ return ; case 'workspace:storage': return ; + case 'workspace:license': + return ( + + ); default: return null; } @@ -52,10 +59,18 @@ 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 showLicense = information?.isOwner && isSelfhosted; const items = useMemo(() => { return [ { @@ -88,10 +103,14 @@ export const useWorkspaceSettingList = (): SettingSidebarItem[] => { icon: , 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: , + testId: 'workspace-setting:license', + }, ].filter((item): item is SettingSidebarItem => !!item); - }, [showBilling, t]); + }, [showBilling, showLicense, t]); return items; }; 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 new file mode 100644 index 0000000000000..f72b8c8dbd1dc --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/index.tsx @@ -0,0 +1,196 @@ +import { Button, Switch } 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 { UrlService } from '@affine/core/modules/url'; +import { WorkspaceService } from '@affine/core/modules/workspace'; +import { + createCustomerPortalMutation, + SubscriptionPlan, + SubscriptionRecurring, +} from '@affine/graphql'; +import { useI18n } from '@affine/i18n'; +import { FrameworkScope, useLiveData, useService } from '@toeverything/infra'; +import { useCallback, useEffect, useState } from 'react'; + +import type { SettingState } from '../../types'; +import { SelfHostTeamCard } from './self-host-team-card'; +import * as styles from './styles.css'; + +export const WorkspaceSettingLicense = ({ + onChangeSettingState, +}: { + onChangeSettingState: (state: SettingState) => void; +}) => { + const workspace = useService(WorkspaceService).workspace; + + const t = useI18n(); + + const subscriptionService = workspace?.scope.get( + WorkspaceSubscriptionService + ); + const subscription = useLiveData( + subscriptionService?.subscription.subscription$ + ); + + // TODO(@JimmFly): add sign in check + + useEffect(() => { + subscriptionService?.subscription.revalidate(); + }, [subscriptionService?.subscription]); + + if (workspace === null) { + return null; + } + + return ( + + + {workspace.flavour !== 'local' ? ( + <> + + + + + {subscription?.end ? ( + + ) : null} + + ) : ( + 'Self-hosted Team workspace depends on cloud workspace, please sign in and enable cloud first.' + )} + + ); +}; + +const ResumeSubscription = ({ expirationDate }: { expirationDate: string }) => { + const t = useI18n(); + const handleClick = useCallback(() => { + window.open('', '_blank'); + }, []); + + return ( + + + + ); +}; + +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 ( + + + + + + ); +}; + +const PaymentMethodUpdater = () => { + const { isMutating, trigger } = useMutation({ + mutation: createCustomerPortalMutation, + }); + const urlService = useService(UrlService); + const t = useI18n(); + + const update = useAsyncCallback(async () => { + await trigger(null, { + onSuccess: data => { + urlService.openPopupWindow(data.createCustomerPortal); + }, + }); + }, [trigger, urlService]); + + return ( + + + + ); +}; + +const Checkout = () => { + const t = useI18n(); + const [recurring, setRecurring] = useState(SubscriptionRecurring.Monthly); + const onCheckout = useCallback(() => { + // https://affine.fail/ + // http://localhost:8080/ + window.open( + `http://localhost:8080/subscribe?product=${recurring === SubscriptionRecurring.Monthly ? 'monthly' : 'yearly'}-selfhost-team`, + '_blank' + ); + }, [recurring]); + const toggleSwitch = useCallback((checked: boolean) => { + if (checked) { + setRecurring(SubscriptionRecurring.Yearly); + return; + } + setRecurring(SubscriptionRecurring.Monthly); + }, []); + return ( + + {t['com.affine.payment.cloud.pricing-plan.toggle-billed-yearly']()} + + } + desc={''} + > + + + ); +}; 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 new file mode 100644 index 0000000000000..b6061b782830b --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/self-host-team-card.tsx @@ -0,0 +1,246 @@ +import { Button, ConfirmModal, Input, notify } from '@affine/component'; +import { SettingRow } from '@affine/component/setting-components'; +import { SelfhostActivateLicenseService } 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 { useCallback, useEffect, useMemo, useState } from 'react'; + +import type { SettingState } from '../../types'; +import * as styles from './styles.css'; + +export const SelfHostTeamCard = ({ + onChangeSettingState, +}: { + onChangeSettingState: (state: SettingState) => void; +}) => { + const t = useI18n(); + + const workspace = useService(WorkspaceService).workspace; + const workspaceQuotaService = useService(WorkspaceQuotaService); + const permission = useService(WorkspacePermissionService).permission; + const isTeam = useLiveData(permission.isTeam$); + const workspaceQuota = useLiveData(workspaceQuotaService.quota.quota$); + + const [openModal, setOpenModal] = useState(false); + const [loading, setLoading] = useState(false); + const selfhostActivateLicenseService = useService( + SelfhostActivateLicenseService + ); + + useEffect(() => { + permission.revalidate(); + workspaceQuotaService.quota.revalidate(); + }, [permission, workspaceQuotaService]); + + const expirationDate = useMemo(() => { + if (isTeam) { + return t[ + 'com.affine.settings.workspace.license.self-host-team.team.description' + ]({ + expirationDate: new Date().toLocaleDateString(), + }); + } + return t[ + 'com.affine.settings.workspace.license.self-host-team.free.description' + ]({ + memberCount: workspaceQuota?.humanReadable.memberLimit || '10', + }); + }, [isTeam, t, workspaceQuota]); + const handleClick = useCallback(() => { + setOpenModal(true); + }, []); + + const jumpToPricePlan = useCallback(() => { + onChangeSettingState({ + activeTab: 'plans', + scrollAnchor: 'TeamPricingPlan', + }); + }, [onChangeSettingState]); + + const onActivate = useCallback( + (license: string) => { + setLoading(true); + selfhostActivateLicenseService + .activateLicense(workspace.id, license) + .then(() => { + setLoading(false); + setOpenModal(false); + permission.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, selfhostActivateLicenseService, t, workspace.id] + ); + + const onDeactivate = useCallback(() => { + setLoading(true); + selfhostActivateLicenseService + .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, selfhostActivateLicenseService, t, workspace.id]); + + const handleConfirm = useCallback( + (license: string) => { + if (isTeam) { + onDeactivate(); + } else { + onActivate(license); + } + }, + [isTeam, onActivate, onDeactivate] + ); + + return ( + <> +
+
+ + + {isTeam ? null : ( + + )} +
+

+ {workspaceQuota?.memberCount} + {isTeam ? '' : `/${workspaceQuota?.memberLimit}`} + + {t['com.affine.settings.workspace.license.self-host-team.seats']()} + +

+
+ + + ); +}; + +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); + }, [key, onConfirm]); + + return ( + + {isTeam ? null : ( + <> + + + + If you encounter any issues, please contact our + + customer support + + . + + + + )} + + ); +}; 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 new file mode 100644 index 0000000000000..475b3e021c711 --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/setting/workspace-setting/license/styles.css.ts @@ -0,0 +1,47 @@ +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', + justifyContent: 'space-between', + padding: '12px', + border: `1px solid ${cssVar('borderColor')}`, + borderRadius: '8px', +}); + +export const currentPlan = style({ + flex: '1 0 0', +}); + +export const planPrice = style({ + fontSize: cssVar('fontH6'), + fontWeight: 600, +}); + +export const activeButton = style({ + marginTop: '8px', + marginRight: '8px', +}); + +export const seat = style({ + marginLeft: '4px', +}); + +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'), +}); diff --git a/packages/frontend/core/src/desktop/pages/subscribe/index.tsx b/packages/frontend/core/src/desktop/pages/subscribe/index.tsx index d41ddd60b1948..8a0008541ed15 100644 --- a/packages/frontend/core/src/desktop/pages/subscribe/index.tsx +++ b/packages/frontend/core/src/desktop/pages/subscribe/index.tsx @@ -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, diff --git a/packages/frontend/core/src/desktop/pages/upgrade-success/self-host-team/index.tsx b/packages/frontend/core/src/desktop/pages/upgrade-success/self-host-team/index.tsx new file mode 100644 index 0000000000000..8c8c9aacafe1e --- /dev/null +++ b/packages/frontend/core/src/desktop/pages/upgrade-success/self-host-team/index.tsx @@ -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 ; + } + if (isMutating || key) { + return ; + } else { + return ( + + ); + } +}; + +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 = ( + + {t['com.affine.payment.license-success.text-1']()} + + + ), + }} + /> + + + ); + return ( + +
+
+ {licenseKey ? licenseKey : } + } + className={styles.icon} + size="20" + tooltip={t['Copy']()} + onClick={onCopy} + /> +
+
{t['com.affine.payment.license-success.hint']()}
+
+ +
+
+
+ ); +}; diff --git a/packages/frontend/core/src/desktop/pages/upgrade-success/self-host-team/styles.css.ts b/packages/frontend/core/src/desktop/pages/upgrade-success/self-host-team/styles.css.ts new file mode 100644 index 0000000000000..474b38e0ebb1e --- /dev/null +++ b/packages/frontend/core/src/desktop/pages/upgrade-success/self-host-team/styles.css.ts @@ -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'), +}); diff --git a/packages/frontend/core/src/desktop/router.tsx b/packages/frontend/core/src/desktop/router.tsx index d311ade62c0ab..b0173c75a36fe 100644 --- a/packages/frontend/core/src/desktop/router.tsx +++ b/packages/frontend/core/src/desktop/router.tsx @@ -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'), diff --git a/packages/frontend/core/src/modules/cloud/entities/workspace-subscription.ts b/packages/frontend/core/src/modules/cloud/entities/workspace-subscription.ts index a97640b98b5e8..ff6fab9de6b17 100644 --- a/packages/frontend/core/src/modules/cloud/entities/workspace-subscription.ts +++ b/packages/frontend/core/src/modules/cloud/entities/workspace-subscription.ts @@ -27,7 +27,9 @@ export class WorkspaceSubscription extends Entity { error$ = new LiveData(null); team$ = this.subscription$.map( - subscription => subscription?.plan === SubscriptionPlan.Team + subscription => + subscription?.plan === SubscriptionPlan.Team || + subscription?.plan === SubscriptionPlan.SelfHostedTeam ); constructor( diff --git a/packages/frontend/core/src/modules/cloud/index.ts b/packages/frontend/core/src/modules/cloud/index.ts index 91abe89d0b595..bf5f9463853eb 100644 --- a/packages/frontend/core/src/modules/cloud/index.ts +++ b/packages/frontend/core/src/modules/cloud/index.ts @@ -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 { SelfhostActivateLicenseService } from './services/selfhost-activate-license'; +export { SelfhostGenerateLicenseService } from './services/selfhost-generate-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 { SelfhostActivateLicenseService } from './services/selfhost-activate-license'; +import { SelfhostGenerateLicenseService } from './services/selfhost-generate-license'; import { ServerService } from './services/server'; import { ServersService } from './services/servers'; import { SubscriptionService } from './services/subscription'; @@ -70,6 +74,7 @@ import { WorkspaceSubscriptionService } from './services/workspace-subscription' import { AuthStore } from './stores/auth'; import { CloudDocMetaStore } from './stores/cloud-doc-meta'; import { InvoicesStore } from './stores/invoices'; +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 +133,10 @@ export function configureCloudModule(framework: Framework) { .store(UserFeatureStore, [GraphQLService]) .service(InvoicesService) .store(InvoicesStore, [GraphQLService]) - .entity(Invoices, [InvoicesStore]); + .entity(Invoices, [InvoicesStore]) + .service(SelfhostGenerateLicenseService, [SelfhostLicenseStore]) + .service(SelfhostActivateLicenseService, [SelfhostLicenseStore]) + .store(SelfhostLicenseStore, [GraphQLService]); framework .scope(WorkspaceScope) diff --git a/packages/frontend/core/src/modules/cloud/services/selfhost-activate-license.ts b/packages/frontend/core/src/modules/cloud/services/selfhost-activate-license.ts new file mode 100644 index 0000000000000..aade79a28437b --- /dev/null +++ b/packages/frontend/core/src/modules/cloud/services/selfhost-activate-license.ts @@ -0,0 +1,16 @@ +import { Service } from '@toeverything/infra'; + +import type { SelfhostLicenseStore } from '../stores/selfhost-license'; + +export class SelfhostActivateLicenseService extends Service { + constructor(private readonly store: SelfhostLicenseStore) { + super(); + } + async activateLicense(workspaceId: string, licenseKey: string) { + return await this.store.activate(workspaceId, licenseKey); + } + + async deactivateLicense(workspaceId: string) { + return await this.store.deactivate(workspaceId); + } +} diff --git a/packages/frontend/core/src/modules/cloud/services/selfhost-generate-license.ts b/packages/frontend/core/src/modules/cloud/services/selfhost-generate-license.ts new file mode 100644 index 0000000000000..9144d8cdf3c40 --- /dev/null +++ b/packages/frontend/core/src/modules/cloud/services/selfhost-generate-license.ts @@ -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 { SelfhostLicenseStore } from '../stores/selfhost-license'; + +export class SelfhostGenerateLicenseService extends Service { + constructor(private readonly store: SelfhostLicenseStore) { + super(); + } + licenseKey$ = new LiveData(null); + isLoading$ = new LiveData(false); + error$ = new LiveData(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); + }) + ); + }) + ); +} diff --git a/packages/frontend/core/src/modules/cloud/stores/selfhost-license.ts b/packages/frontend/core/src/modules/cloud/stores/selfhost-license.ts new file mode 100644 index 0000000000000..7ba7beb185044 --- /dev/null +++ b/packages/frontend/core/src/modules/cloud/stores/selfhost-license.ts @@ -0,0 +1,57 @@ +import { + activateLicenseMutation, + deactivateLicenseMutation, + generateLicenseKeyMutation, +} from '@affine/graphql'; +import { Store } from '@toeverything/infra'; + +import type { GraphQLService } from '../services/graphql'; + +export class SelfhostLicenseStore extends Store { + constructor(private readonly gqlService: GraphQLService) { + super(); + } + + async generateKey(sessionId: string, signal?: AbortSignal): Promise { + const data = await this.gqlService.gql({ + query: generateLicenseKeyMutation, + variables: { + sessionId: sessionId, + }, + context: { + signal, + }, + }); + + return data.generateLicenseKey; + } + + async activate(workspaceId: string, license: string, signal?: AbortSignal) { + const data = await this.gqlService.gql({ + query: activateLicenseMutation, + variables: { + workspaceId: workspaceId, + license: license, + }, + context: { + signal, + }, + }); + + return data.activateLicense; + } + + async deactivate(workspaceId: string, signal?: AbortSignal) { + const data = await this.gqlService.gql({ + query: deactivateLicenseMutation, + variables: { + workspaceId: workspaceId, + }, + context: { + signal, + }, + }); + + return data.deactivateLicense; + } +} diff --git a/packages/frontend/core/src/modules/permissions/entities/permission.ts b/packages/frontend/core/src/modules/permissions/entities/permission.ts index 0b12f81c56de1..a4f4e59a8b1ee 100644 --- a/packages/frontend/core/src/modules/permissions/entities/permission.ts +++ b/packages/frontend/core/src/modules/permissions/entities/permission.ts @@ -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( diff --git a/packages/frontend/graphql/src/graphql/activate-license.gql b/packages/frontend/graphql/src/graphql/activate-license.gql new file mode 100644 index 0000000000000..3434fa52b7227 --- /dev/null +++ b/packages/frontend/graphql/src/graphql/activate-license.gql @@ -0,0 +1,6 @@ +mutation activateLicense($workspaceId: String!, $license: String!) { + activateLicense(workspaceId: $workspaceId, license: $license) { + installedAt + validatedAt + } +} \ No newline at end of file diff --git a/packages/frontend/graphql/src/graphql/deactivate-license.gql b/packages/frontend/graphql/src/graphql/deactivate-license.gql new file mode 100644 index 0000000000000..304ca77820e42 --- /dev/null +++ b/packages/frontend/graphql/src/graphql/deactivate-license.gql @@ -0,0 +1,3 @@ +mutation deactivateLicense($workspaceId: String!) { + deactivateLicense(workspaceId: $workspaceId) +} \ No newline at end of file diff --git a/packages/frontend/graphql/src/graphql/generate-license-key.gql b/packages/frontend/graphql/src/graphql/generate-license-key.gql new file mode 100644 index 0000000000000..43f66dbfabd1a --- /dev/null +++ b/packages/frontend/graphql/src/graphql/generate-license-key.gql @@ -0,0 +1,3 @@ +mutation generateLicenseKey($sessionId: String!) { + generateLicenseKey(sessionId: $sessionId) +} diff --git a/packages/frontend/graphql/src/graphql/index.ts b/packages/frontend/graphql/src/graphql/index.ts index ce583d93c5dcb..1e47a100e8472 100644 --- a/packages/frontend/graphql/src/graphql/index.ts +++ b/packages/frontend/graphql/src/graphql/index.ts @@ -18,6 +18,20 @@ fragment CredentialsRequirements on CredentialsRequirementType { ...PasswordLimits } }` +export const activateLicenseMutation = { + id: 'activateLicenseMutation' as const, + operationName: 'activateLicense', + definitionName: 'activateLicense', + containsFile: false, + query: ` +mutation activateLicense($workspaceId: String!, $license: String!) { + activateLicense(workspaceId: $workspaceId, license: $license) { + installedAt + validatedAt + } +}`, +}; + export const adminServerConfigQuery = { id: 'adminServerConfigQuery' as const, operationName: 'adminServerConfig', @@ -256,6 +270,17 @@ mutation createWorkspace { }`, }; +export const deactivateLicenseMutation = { + id: 'deactivateLicenseMutation' as const, + operationName: 'deactivateLicense', + definitionName: 'deactivateLicense', + containsFile: false, + query: ` +mutation deactivateLicense($workspaceId: String!) { + deactivateLicense(workspaceId: $workspaceId) +}`, +}; + export const deleteAccountMutation = { id: 'deleteAccountMutation' as const, operationName: 'deleteAccount', @@ -304,6 +329,17 @@ mutation forkCopilotSession($options: ForkChatSessionInput!) { }`, }; +export const generateLicenseKeyMutation = { + id: 'generateLicenseKeyMutation' as const, + operationName: 'generateLicenseKey', + definitionName: 'generateLicenseKey', + containsFile: false, + query: ` +mutation generateLicenseKey($sessionId: String!) { + generateLicenseKey(sessionId: $sessionId) +}`, +}; + export const getCopilotHistoriesQuery = { id: 'getCopilotHistoriesQuery' as const, operationName: 'getCopilotHistories', diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts index b6eac30be6fc4..b2a11f5bcd249 100644 --- a/packages/frontend/graphql/src/schema.ts +++ b/packages/frontend/graphql/src/schema.ts @@ -533,6 +533,12 @@ export interface InvoiceType { updatedAt: Scalars['DateTime']['output']; } +export interface License { + __typename?: 'License'; + installedAt: Scalars['DateTime']['output']; + validatedAt: Scalars['DateTime']['output']; +} + export interface LimitedUserType { __typename?: 'LimitedUserType'; /** User email */ @@ -574,6 +580,7 @@ export interface MissingOauthQueryParameterDataType { export interface Mutation { __typename?: 'Mutation'; acceptInviteById: Scalars['Boolean']['output']; + activateLicense: License; addWorkspaceFeature: Scalars['Int']['output']; approveMember: Scalars['String']['output']; cancelSubscription: SubscriptionType; @@ -598,6 +605,7 @@ export interface Mutation { createUser: UserType; /** Create a new workspace */ createWorkspace: WorkspaceType; + deactivateLicense: Scalars['Boolean']['output']; deleteAccount: DeleteAccount; deleteBlob: Scalars['Boolean']['output']; /** Delete a user account */ @@ -656,6 +664,11 @@ export interface MutationAcceptInviteByIdArgs { workspaceId: Scalars['String']['input']; } +export interface MutationActivateLicenseArgs { + license: Scalars['String']['input']; + workspaceId: Scalars['String']['input']; +} + export interface MutationAddWorkspaceFeatureArgs { feature: FeatureType; workspaceId: Scalars['String']['input']; @@ -725,6 +738,10 @@ export interface MutationCreateWorkspaceArgs { init?: InputMaybe; } +export interface MutationDeactivateLicenseArgs { + workspaceId: Scalars['String']['input']; +} + export interface MutationDeleteBlobArgs { hash?: InputMaybe; key?: InputMaybe; @@ -1396,6 +1413,8 @@ export interface WorkspaceType { /** Get user invoice count */ invoiceCount: Scalars['Int']['output']; invoices: Array; + /** The selfhost license of the workspace */ + license: Maybe; /** member count of workspace */ memberCount: Scalars['Int']['output']; /** Members of workspace */ @@ -1461,6 +1480,20 @@ export interface TokenType { token: Scalars['String']['output']; } +export type ActivateLicenseMutationVariables = Exact<{ + workspaceId: Scalars['String']['input']; + license: Scalars['String']['input']; +}>; + +export type ActivateLicenseMutation = { + __typename?: 'Mutation'; + activateLicense: { + __typename?: 'License'; + installedAt: string; + validatedAt: string; + }; +}; + export type AdminServerConfigQueryVariables = Exact<{ [key: string]: never }>; export type AdminServerConfigQuery = { @@ -1669,6 +1702,15 @@ export type CreateWorkspaceMutation = { }; }; +export type DeactivateLicenseMutationVariables = Exact<{ + workspaceId: Scalars['String']['input']; +}>; + +export type DeactivateLicenseMutation = { + __typename?: 'Mutation'; + deactivateLicense: boolean; +}; + export type DeleteAccountMutationVariables = Exact<{ [key: string]: never }>; export type DeleteAccountMutation = { @@ -1718,6 +1760,15 @@ export type PasswordLimitsFragment = { maxLength: number; }; +export type GenerateLicenseKeyMutationVariables = Exact<{ + sessionId: Scalars['String']['input']; +}>; + +export type GenerateLicenseKeyMutation = { + __typename?: 'Mutation'; + generateLicenseKey: string; +}; + export type GetCopilotHistoriesQueryVariables = Exact<{ workspaceId: Scalars['String']['input']; docId?: InputMaybe; @@ -3058,6 +3109,11 @@ export type Queries = }; export type Mutations = + | { + name: 'activateLicenseMutation'; + variables: ActivateLicenseMutationVariables; + response: ActivateLicenseMutation; + } | { name: 'deleteBlobMutation'; variables: DeleteBlobMutationVariables; @@ -3133,6 +3189,11 @@ export type Mutations = variables: CreateWorkspaceMutationVariables; response: CreateWorkspaceMutation; } + | { + name: 'deactivateLicenseMutation'; + variables: DeactivateLicenseMutationVariables; + response: DeactivateLicenseMutation; + } | { name: 'deleteAccountMutation'; variables: DeleteAccountMutationVariables; @@ -3153,6 +3214,11 @@ export type Mutations = variables: ForkCopilotSessionMutationVariables; response: ForkCopilotSessionMutation; } + | { + name: 'generateLicenseKeyMutation'; + variables: GenerateLicenseKeyMutationVariables; + response: GenerateLicenseKeyMutation; + } | { name: 'leaveWorkspaceMutation'; variables: LeaveWorkspaceMutationVariables; diff --git a/packages/frontend/i18n/src/i18n-completenesses.json b/packages/frontend/i18n/src/i18n-completenesses.json index 3a60b196129d0..4b701e65454fe 100644 --- a/packages/frontend/i18n/src/i18n-completenesses.json +++ b/packages/frontend/i18n/src/i18n-completenesses.json @@ -1,26 +1,26 @@ { - "ar": 98, + "ar": 99, "ca": 5, "da": 5, - "de": 98, - "el-GR": 98, + "de": 99, + "el-GR": 99, "en": 100, - "es-AR": 98, + "es-AR": 99, "es-CL": 100, - "es": 98, - "fa": 98, - "fr": 98, + "es": 99, + "fa": 99, + "fr": 99, "hi": 2, - "it-IT": 98, + "it-IT": 99, "it": 1, - "ja": 98, + "ja": 99, "ko": 71, - "pl": 98, - "pt-BR": 98, - "ru": 98, - "sv-SE": 98, - "uk": 98, + "pl": 99, + "pt-BR": 99, + "ru": 99, + "sv-SE": 99, + "uk": 99, "ur": 2, - "zh-Hans": 98, - "zh-Hant": 98 + "zh-Hans": 99, + "zh-Hant": 99 } diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index bf98546433a66..7b9c89610555c 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -4185,6 +4185,26 @@ export function useAFFiNEI18N(): { * `Congratulations! Your workspace has been successfully upgraded to a Team Workspace. Now you can invite unlimited members to collaborate in this workspace.` */ ["com.affine.payment.upgrade-success-page.team.text-1"](): string; + /** + * `Thank you for your purchase!` + */ + ["com.affine.payment.license-success.title"](): string; + /** + * `Thank you for purchasing the AFFiNE self-hosted license.` + */ + ["com.affine.payment.license-success.text-1"](): string; + /** + * `You can use this key to upgrade in Settings > Workspace > Billing > Upgrade` + */ + ["com.affine.payment.license-success.hint"](): string; + /** + * `Open AFFiNE` + */ + ["com.affine.payment.license-success.open-affine"](): string; + /** + * `Copied key to clipboard` + */ + ["com.affine.payment.license-success.copy"](): string; /** * `Close` */ @@ -5405,6 +5425,70 @@ export function useAFFiNEI18N(): { * `Cancel Plan` */ ["com.affine.settings.workspace.billing.team-workspace.cancel-plan"](): string; + /** + * `License` + */ + ["com.affine.settings.workspace.license"](): string; + /** + * `You can view the license information of the workspace here` + */ + ["com.affine.settings.workspace.license.description"](): string; + /** + * `Self-host Team Workspace` + */ + ["com.affine.settings.workspace.license.self-host-team"](): string; + /** + * `This license will expire on {{expirationDate}}.` + */ + ["com.affine.settings.workspace.license.self-host-team.team.description"](options: { + readonly expirationDate: string; + }): string; + /** + * `Basic version: {{memberCount}} seats. For more, purchase or use activation key.` + */ + ["com.affine.settings.workspace.license.self-host-team.free.description"](options: { + readonly memberCount: string; + }): string; + /** + * `Seats` + */ + ["com.affine.settings.workspace.license.self-host-team.seats"](): string; + /** + * `Active key` + */ + ["com.affine.settings.workspace.license.self-host-team.active-key"](): string; + /** + * `Deactivate` + */ + ["com.affine.settings.workspace.license.self-host-team.deactivate-license"](): string; + /** + * `Buy more seat` + */ + ["com.affine.settings.workspace.license.buy-more-seat"](): string; + /** + * `Activate License` + */ + ["com.affine.settings.workspace.license.activate-modal.title"](): string; + /** + * `Enter license key to activate this self host workspace.` + */ + ["com.affine.settings.workspace.license.activate-modal.description"](): string; + /** + * `License activated successfully.` + */ + ["com.affine.settings.workspace.license.activate-success"](): string; + /** + * `Deactivate License` + */ + ["com.affine.settings.workspace.license.deactivate-modal.title"](): string; + /** + * `Are you sure you want to deactivate this license?` + */ + ["com.affine.settings.workspace.license.deactivate-modal.description"](): string; + /** + * `License deactivated successfully.` + */ + ["com.affine.settings.workspace.license.deactivate-success"](): string; /** * `Local` */ @@ -6937,6 +7021,12 @@ export const TypedTrans: { ["com.affine.payment.upgrade-success-page.team.text-2"]: ComponentType, { ["1"]: JSX.Element; }>>; + /** + * `If you have any questions, please contact our <1>customer support.` + */ + ["com.affine.payment.license-success.text-2"]: ComponentType, { + ["1"]: JSX.Element; + }>>; /** * `This action deletes the old Favorites section. Your documents are safe, ensure you've moved your frequently accessed documents to the new personal Favorites section.` */ @@ -6976,6 +7066,12 @@ export const TypedTrans: { ["1"]: JSX.Element; ["2"]: JSX.Element; }>>; + /** + * `If you encounter any issues, please contact our <1>customer support.` + */ + ["com.affine.settings.workspace.license.activate-modal.tips"]: 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 19bfd8b0b66ff..0f74bc498e654 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1038,6 +1038,12 @@ "com.affine.payment.upgrade-success-page.title": "Upgrade successful!", "com.affine.payment.upgrade-success-page.team.text-1": "Congratulations! Your workspace has been successfully upgraded to a Team Workspace. Now you can invite unlimited members to collaborate in this workspace.", "com.affine.payment.upgrade-success-page.team.text-2": "If you have any questions, please contact our <1>customer support.", + "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.open-affine": "Open AFFiNE", + "com.affine.payment.license-success.copy": "Copied key to clipboard", "com.affine.peek-view-controls.close": "Close", "com.affine.peek-view-controls.open-doc": "Open this doc", "com.affine.peek-view-controls.open-doc-in-new-tab": "Open in new tab", @@ -1347,6 +1353,22 @@ "com.affine.settings.workspace.billing.team-workspace.not-renewed": "Your subscription will end on {{date}}", "com.affine.settings.workspace.billing.team-workspace.next-billing-date": "Next billing date: {{date}}", "com.affine.settings.workspace.billing.team-workspace.cancel-plan": "Cancel Plan", + "com.affine.settings.workspace.license": "License", + "com.affine.settings.workspace.license.description": "You can view the license information of the workspace here", + "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}}.", + "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.deactivate-license": "Deactivate", + "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.", + "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-success": "License deactivated successfully.", "com.affine.settings.workspace.state.local": "Local", "com.affine.settings.workspace.state.sync-affine-cloud": "Sync with AFFiNE Cloud", "com.affine.settings.workspace.state.self-hosted": "Self-Hosted Server",