Skip to content

Commit 032b69e

Browse files
authored
perf: billing styles and interaction (#1937)
* perf: update billing styles and interaction * perf: layout style * chore: update i18n * feat: notify admins of license expiration
1 parent 53fa546 commit 032b69e

File tree

19 files changed

+142
-23
lines changed

19 files changed

+142
-23
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
import { getEnterpriseLicenseStatus } from '@teable/openapi';
3+
import { ReactQueryKeys } from '@teable/sdk/config';
4+
import { useSession } from '@teable/sdk/hooks';
5+
import { Button } from '@teable/ui-lib/shadcn';
6+
import { toast } from '@teable/ui-lib/shadcn/ui/sonner';
7+
import Link from 'next/link';
8+
import { useTranslation } from 'next-i18next';
9+
import { useEffect, useRef } from 'react';
10+
import { useIsEE } from '@/features/app/hooks/useIsEE';
11+
12+
export const LicenseExpiryBanner = () => {
13+
const { t } = useTranslation('common');
14+
const { user } = useSession();
15+
const isEE = useIsEE();
16+
const toastShownRef = useRef(false);
17+
18+
const shouldCheck = Boolean(isEE && user?.isAdmin);
19+
20+
const { data: licenseStatus } = useQuery({
21+
queryKey: ReactQueryKeys.getEnterpriseLicenseStatus(),
22+
queryFn: () => getEnterpriseLicenseStatus().then(({ data }) => data),
23+
enabled: shouldCheck,
24+
});
25+
26+
const { expiredTime } = licenseStatus ?? {};
27+
28+
useEffect(() => {
29+
if (!shouldCheck || !expiredTime || toastShownRef.current) return;
30+
31+
toast.warning(
32+
<div className="flex w-full items-center justify-between gap-4">
33+
{t('billing.licenseExpiredGracePeriod', {
34+
expiredTime: new Date(expiredTime).toLocaleDateString(),
35+
})}
36+
<Link href="/admin/license" target="_blank">
37+
<Button>{t('actions.update')}</Button>
38+
</Link>
39+
</div>,
40+
{
41+
duration: Infinity,
42+
closeButton: true,
43+
position: 'top-center',
44+
className: 'sm:w-[672px] w-full',
45+
}
46+
);
47+
toastShownRef.current = true;
48+
}, [shouldCheck, expiredTime, t]);
49+
50+
return null;
51+
};

apps/nextjs-app/src/features/app/components/SpaceSettingContainer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const SpaceSettingContainer = ({
2121
}: SpaceSettingContainerProps) => {
2222
return (
2323
<div className={cn('h-screen w-full overflow-y-auto overflow-x-hidden', wrapperClassName)}>
24-
<div className={cn('w-full px-8 py-6', headerClassName)}>
24+
<div className={cn('w-full px-4 py-6 sm:px-8', headerClassName)}>
2525
<div className={cn('border-b pb-4', titleClassName)}>
2626
<h1 className="text-3xl font-semibold">{title}</h1>
2727
{description && <div className="mt-3 text-sm text-slate-500">{description}</div>}

apps/nextjs-app/src/features/app/components/billing/UpgradeWrapper.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { Role } from '@teable/core';
22
import { BillingProductLevel } from '@teable/openapi';
3+
import { UsageLimitModalType, useUsageLimitModalStore } from '@teable/sdk/components/billing/store';
34
import { useBase } from '@teable/sdk/hooks';
45
import type { Base } from '@teable/sdk/model';
56
import { toast } from '@teable/ui-lib/shadcn/ui/sonner';
6-
import { useRouter } from 'next/router';
77
import { useTranslation } from 'next-i18next';
88
import { useMemo, useCallback, type ReactElement, cloneElement } from 'react';
99
import { useBillingLevel } from '../../hooks/useBillingLevel';
@@ -51,12 +51,12 @@ export const UpgradeWrapper: React.FC<IUpgradeWrapperProps> = ({
5151
targetBillingLevel,
5252
onUpgradeClick,
5353
}) => {
54-
const router = useRouter();
5554
const isCloud = useIsCloud();
5655
const isCommunity = useIsCommunity();
5756
const isEE = useIsEE();
5857
const base = useBase() as Base | undefined;
5958
const { t } = useTranslation('common');
59+
const { openModal } = useUsageLimitModalStore();
6060
spaceId = base?.spaceId ?? spaceId;
6161
const baseId = base?.id;
6262
// EE starts from pro level
@@ -95,15 +95,14 @@ export const UpgradeWrapper: React.FC<IUpgradeWrapperProps> = ({
9595
return;
9696
}
9797

98-
router.push(`/space/${spaceId}/setting/plan`);
98+
openModal(UsageLimitModalType.Upgrade);
9999
} else {
100100
window.open('https://app.teable.ai/public/pricing?host=self-hosted', '_blank');
101101
}
102-
}, [onUpgradeClick, isCloud, spaceId, isSpaceOwner, t, router]);
102+
}, [isCloud, spaceId, isSpaceOwner, t, openModal, onUpgradeClick]);
103103

104104
const billingConfig = useBillingLevelConfig(targetBillingLevel);
105105

106-
// 创建badge组件
107106
const badge = useMemo(() => {
108107
if (!needsUpgrade) {
109108
return null;

apps/nextjs-app/src/features/app/hooks/useBillingLevelConfig.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,20 @@ export const useBillingLevelConfig = (productLevel?: BillingProductLevel) => {
1111
[BillingProductLevel.Free]: {
1212
name: t('level.free'),
1313
description: t('billing.levelTips', { level: t('level.free') }),
14-
tagCls: 'bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-white',
15-
upgradeTagCls:
16-
'border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-white',
14+
tagCls: 'bg-gray-200 dark:bg-gray-700 text-gray-600 dark:text-white',
15+
upgradeTagCls: 'border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-white',
1716
},
1817
[BillingProductLevel.Plus]: {
1918
name: t('level.plus'),
2019
description: t('billing.levelTips', { level: t('level.plus') }),
21-
tagCls: 'bg-violet-200 dark:bg-violet-700 text-violet-600 dark:text-white',
22-
upgradeTagCls: 'border border-violet-200 dark:border-violet-700 text-violet-600',
20+
tagCls: 'bg-emerald-200 dark:bg-emerald-700 text-emerald-600 dark:text-white',
21+
upgradeTagCls: 'border border-emerald-200 dark:border-emerald-700 text-emerald-600',
2322
},
2423
[BillingProductLevel.Pro]: {
2524
name: t('level.pro'),
2625
description: t('billing.levelTips', { level: t('level.pro') }),
27-
tagCls: 'bg-amber-200 dark:bg-amber-700 text-amber-600 dark:text-white',
28-
upgradeTagCls: 'border border-amber-200 dark:border-amber-700 text-amber-600',
26+
tagCls: 'bg-blue-200 dark:bg-blue-700 text-blue-600 dark:text-white',
27+
upgradeTagCls: 'border border-blue-200 dark:border-blue-700 text-blue-600',
2928
},
3029
[BillingProductLevel.Enterprise]: {
3130
name: t('level.enterprise'),

apps/nextjs-app/src/features/app/layouts/SpaceLayout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { NotificationProvider, SessionProvider } from '@teable/sdk';
44
import { AppProvider } from '@teable/sdk/context';
55
import { useTranslation } from 'next-i18next';
66
import React, { Fragment } from 'react';
7+
import { LicenseExpiryBanner } from '@/features/app/components/LicenseExpiryBanner';
78
import { AppLayout } from '@/features/app/layouts';
89
import { SpaceSideBar } from '../blocks/space/space-side-bar/SpaceSideBar';
910
import { Sidebar } from '../components/sidebar/Sidebar';
@@ -26,6 +27,7 @@ export const SpaceLayout: React.FC<{
2627
<AppProvider locale={sdkLocale} lang={i18n.language} dehydratedState={dehydratedState}>
2728
<SessionProvider user={user}>
2829
<NotificationProvider>
30+
<LicenseExpiryBanner />
2931
<div id="portal" className="relative flex h-screen w-full items-start">
3032
<Sidebar headerLeft={<SidebarHeaderLeft />}>
3133
<Fragment>

packages/common-i18n/src/locales/de/common.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@
4545
"more": "Mehr",
4646
"move": "Verschieben nach",
4747
"turnOn": "Einschalten",
48-
"exit": "Ausloggen"
48+
"exit": "Ausloggen",
49+
"next": "Nächste",
50+
"previous": "Vorherige"
4951
},
5052
"quickAction": {
5153
"title": "Schnelle Aktionen...",
@@ -250,6 +252,7 @@
250252
"unavailableInPlanTips": "Der aktuelle Abonnementplan unterstützt diese Funktion nicht",
251253
"unavailableConnectionTips": "Die Datenbankverbindungsfunktion wird in Zukunft entfernt und ist nur in der Enterprise-Edition von öffentlicher Cloud und selbstgehosteten Versionen verfügbar.",
252254
"levelTips": "Dieser Space befindet sich derzeit auf dem {{level}} Plan",
255+
"licenseExpiredGracePeriod": "Ihre Self-Hosted-Lizenz ist abgelaufen und wird am {{expiredTime}} auf den kostenlosen Plan herabgestuft. Bitte aktualisieren Sie Ihre Lizenz umgehend, um Zugriff auf Premium-Funktionen zu behalten.",
253256
"spaceSubscriptionModal": {
254257
"title": "Upgrade des Space-Abonnementplans",
255258
"description": "Sie können nur Arbeitsbereiche aktualisieren, deren Eigentümer Sie sind."

packages/common-i18n/src/locales/en/common.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@
4949
"deleteTip": "Are you sure you want to delete \"{{name}}\"?",
5050
"move": "Move to",
5151
"turnOn": "Turn on",
52-
"exit": "Exit"
52+
"exit": "Exit",
53+
"next": "Next",
54+
"previous": "Previous"
5355
},
5456
"quickAction": {
5557
"title": "Quick Actions...",
@@ -624,6 +626,7 @@
624626
"authorityMatrixRequiresUpgrade": "Upgrade to Enterprise Edition (EE) to enable authority matrix",
625627
"viewPricing": "View Pricing",
626628
"billable": "Billable",
629+
"licenseExpiredGracePeriod": "Your self-hosted license has expired and will downgrade to the free plan on {{expiredTime}}. Please update your license promptly to retain access to premium features.",
627630
"spaceSubscriptionModal": {
628631
"title": "Upgrade space's subscription plan",
629632
"description": "You can only upgrade spaces where you are an owner"

packages/common-i18n/src/locales/es/common.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@
4545
"more": "Más",
4646
"move": "Mover a",
4747
"turnOn": "Activar",
48-
"exit": "Cerrar sesión"
48+
"exit": "Cerrar sesión",
49+
"next": "Siguiente",
50+
"previous": "Anterior"
4951
},
5052
"quickAction": {
5153
"title": "Acciones Rápidas...",
@@ -233,6 +235,7 @@
233235
"userLimitExceededDescription": "La instancia actual ha alcanzado el número máximo de usuarios permitidos por tu licencia. Por favor, desactiva algunos usuarios o actualiza la licencia.",
234236
"unavailableInPlanTips": "El plan de suscripción actual no admite esta función",
235237
"unavailableConnectionTips": "La función de conexión de base de datos se eliminará en el futuro y solo estará disponible en la edición Enterprise y en versiones autohospedadas.",
238+
"licenseExpiredGracePeriod": "Su licencia autohospedada ha caducado y se degradará al plan gratuito el {{expiredTime}}. Actualice su licencia de inmediato para mantener el acceso a las funciones premium.",
236239
"spaceSubscriptionModal": {
237240
"title": "Plan de suscripción de actualización de Space",
238241
"description": "Solo puede actualizar los espacios de trabajo donde es un propietario"

packages/common-i18n/src/locales/fr/common.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@
3838
"more": "Plus",
3939
"move": "Déplacer vers",
4040
"turnOn": "Activer",
41-
"exit": "Déconnexion"
41+
"exit": "Déconnexion",
42+
"next": "Suivant",
43+
"previous": "Précédent"
4244
},
4345
"quickAction": {
4446
"title": "Actions rapides...",
@@ -208,6 +210,7 @@
208210
"unavailableInPlanTips": "Le plan d'abonnement actuel ne prend pas en charge cette fonctionnalité",
209211
"unavailableConnectionTips": "La fonctionnalité de connexion de base de données sera supprimée et ne sera disponible que dans la version Entreprise et les versions auto-hébergées.",
210212
"levelTips": "Cet espace est actuellement sur le plan {{level}}",
213+
"licenseExpiredGracePeriod": "Votre licence auto-hébergée a expiré et sera rétrogradée au forfait gratuit le {{expiredTime}}. Veuillez mettre à jour votre licence rapidement pour conserver l'accès aux fonctionnalités premium.",
211214
"spaceSubscriptionModal": {
212215
"title": "Mettre à niveau le plan d'abonnement de l'espace",
213216
"description": "Vous ne pouvez mettre à niveau que les espaces où vous êtes propriétaire"

packages/common-i18n/src/locales/it/common.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@
4545
"more": "Altro",
4646
"move": "Sposta in",
4747
"turnOn": "Attiva",
48-
"exit": "Esci"
48+
"exit": "Esci",
49+
"next": "Successivo",
50+
"previous": "Precedente"
4951
},
5052
"quickAction": {
5153
"title": "Azioni rapide...",
@@ -247,6 +249,7 @@
247249
"unavailableInPlanTips": "Il piano di abbonamento attuale non supporta questa funzione",
248250
"unavailableConnectionTips": "La funzione di connessione al database verrà rimossa in futuro e sarà disponibile solo nella versione Enterprise e nelle versioni self-hosted.",
249251
"levelTips": "Questo spazio è attualmente sul piano {{level}}",
252+
"licenseExpiredGracePeriod": "La tua licenza self-hosted è scaduta e verrà declassata al piano gratuito il {{expiredTime}}. Aggiorna la tua licenza tempestivamente per mantenere l'accesso alle funzionalità premium.",
250253
"spaceSubscriptionModal": {
251254
"title": "Aggiorna il piano di abbonamento dello spazio",
252255
"description": "Puoi aggiornare solo gli spazi di lavoro di cui sei proprietario"

0 commit comments

Comments
 (0)