mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-22 08:25:21 +08:00
fix(cloud): retire beta banner and use production subscription links
This commit is contained in:
@@ -1,15 +1,9 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { useDocumentTitle } from '@/hooks/useDocumentTitle';
|
||||
import { BetaBanner } from '@/components/BetaBanner';
|
||||
|
||||
// Top-level route layout: drives the dynamic document title from the active
|
||||
// route and renders the matched child route via <Outlet />.
|
||||
export default function RootLayout() {
|
||||
useDocumentTitle();
|
||||
return (
|
||||
<>
|
||||
<BetaBanner />
|
||||
<Outlet />
|
||||
</>
|
||||
);
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ export function cloudPlanCheckoutURL(
|
||||
): string {
|
||||
const url = spaceURL(base, '/cloud');
|
||||
url.search = new URLSearchParams({
|
||||
environment: 'beta',
|
||||
environment: 'new',
|
||||
step: 'plan',
|
||||
workspace,
|
||||
plan,
|
||||
@@ -121,7 +121,7 @@ export function cloudSubscriptionManagementURL(
|
||||
): string {
|
||||
const url = spaceURL(base, '/cloud');
|
||||
url.search = new URLSearchParams({
|
||||
environment: 'beta',
|
||||
environment: 'new',
|
||||
step: 'subscription',
|
||||
workspace,
|
||||
}).toString();
|
||||
|
||||
@@ -1059,10 +1059,7 @@ export default function WizardPage() {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div
|
||||
style={{ top: 'var(--beta-banner-height, 0px)' }}
|
||||
className="fixed inset-x-0 bottom-0 z-50 bg-background flex items-center justify-center"
|
||||
>
|
||||
<div className="fixed inset-0 z-50 bg-background flex items-center justify-center">
|
||||
<LoadingSpinner text={t('wizard.loading')} />
|
||||
</div>
|
||||
);
|
||||
@@ -1076,10 +1073,7 @@ export default function WizardPage() {
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ top: 'var(--beta-banner-height, 0px)' }}
|
||||
className="fixed inset-x-0 bottom-0 z-50 bg-background flex flex-col"
|
||||
>
|
||||
<div className="fixed inset-0 z-50 bg-background flex flex-col">
|
||||
{/* Top bar: Skip button */}
|
||||
<div className="shrink-0 flex items-center justify-between px-4 sm:px-6 py-3 border-b">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AlertCircle, X } from 'lucide-react';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { backendClient } from '@/app/infra/http';
|
||||
|
||||
const RETRY_DELAYS_MS = [1000, 2000, 4000, 8000];
|
||||
const CLOUD_BETA_RETRY_LIMIT = RETRY_DELAYS_MS.length;
|
||||
|
||||
export function BetaBanner() {
|
||||
const { t } = useTranslation();
|
||||
const bannerRef = useRef<HTMLDivElement>(null);
|
||||
const [show, setShow] = useState(false);
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const visible = show && !dismissed;
|
||||
|
||||
// Publish the banner height so the fixed app shell (sidebar + inset) starts
|
||||
// below the notice instead of painting over it.
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (!visible) {
|
||||
root.style.removeProperty('--beta-banner-height');
|
||||
return;
|
||||
}
|
||||
const element = bannerRef.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
const sync = () => {
|
||||
root.style.setProperty(
|
||||
'--beta-banner-height',
|
||||
`${element.getBoundingClientRect().height}px`,
|
||||
);
|
||||
};
|
||||
sync();
|
||||
const observer = new ResizeObserver(sync);
|
||||
observer.observe(element);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
root.style.removeProperty('--beta-banner-height');
|
||||
};
|
||||
}, [visible]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
let retryTimer: number | undefined;
|
||||
|
||||
const checkBetaStatus = async (attempt = 0) => {
|
||||
try {
|
||||
const info = await backendClient.getSystemInfo();
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
setShow(info.deployment_mode === 'cloud' && info.beta === true);
|
||||
} catch {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
// A single failed probe (cold start, restarting backend, flaky
|
||||
// network) must not hide the banner for the rest of the session, so
|
||||
// keep the last known state and retry with backoff.
|
||||
if (attempt < CLOUD_BETA_RETRY_LIMIT) {
|
||||
retryTimer = window.setTimeout(
|
||||
() => void checkBetaStatus(attempt + 1),
|
||||
RETRY_DELAYS_MS[attempt],
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void checkBetaStatus();
|
||||
|
||||
const recheck = () => {
|
||||
if (!cancelled) {
|
||||
void checkBetaStatus();
|
||||
}
|
||||
};
|
||||
window.addEventListener('focus', recheck);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(retryTimer);
|
||||
window.removeEventListener('focus', recheck);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The cloud product page opens the dedicated (paid v1) environment tab;
|
||||
// space.langbot.app resolves the locale itself, so no path prefix applies.
|
||||
const cloudUrl = 'https://space.langbot.app/cloud?environment=dedicated';
|
||||
const ossUrl = 'https://github.com/langbot-app/LangBot';
|
||||
|
||||
return (
|
||||
<Alert
|
||||
ref={bannerRef}
|
||||
className="relative z-30 rounded-none border-x-0 border-t-0 bg-amber-50 dark:bg-amber-950/20 border-amber-200 dark:border-amber-900/50 py-2"
|
||||
>
|
||||
<AlertCircle className="text-amber-600 dark:text-amber-500" />
|
||||
<AlertDescription className="text-sm text-amber-900 dark:text-amber-100">
|
||||
<div className="flex w-full items-start gap-3">
|
||||
<span className="flex-1">
|
||||
{t('beta_banner.message')}{' '}
|
||||
<a
|
||||
href={cloudUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-amber-700 dark:hover:text-amber-300 font-medium"
|
||||
>
|
||||
{t('beta_banner.cloud_link')}
|
||||
</a>{' '}
|
||||
{t('beta_banner.or')}{' '}
|
||||
<a
|
||||
href={ossUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-amber-700 dark:hover:text-amber-300 font-medium"
|
||||
>
|
||||
{t('beta_banner.oss_link')}
|
||||
</a>
|
||||
{t('beta_banner.period')}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setDismissed(true)}
|
||||
className="shrink-0 text-amber-600 dark:text-amber-500 hover:text-amber-800 dark:hover:text-amber-300 p-1 -mt-1 -mr-2"
|
||||
aria-label={t('beta_banner.dismiss')}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -203,9 +203,7 @@ function SidebarProvider({
|
||||
{
|
||||
'--sidebar-width': `${sidebarWidth}px`,
|
||||
'--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
|
||||
// Global notices (e.g. the beta banner) publish their height so
|
||||
// the shell starts below them instead of underneath.
|
||||
height: 'calc(100svh - var(--beta-banner-height, 0px))',
|
||||
height: '100svh',
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
@@ -302,8 +300,8 @@ function Sidebar({
|
||||
<div
|
||||
data-slot="sidebar-container"
|
||||
style={{
|
||||
top: 'var(--beta-banner-height, 0px)',
|
||||
height: 'calc(100svh - var(--beta-banner-height, 0px))',
|
||||
top: 0,
|
||||
height: '100svh',
|
||||
}}
|
||||
className={cn(
|
||||
'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex',
|
||||
|
||||
@@ -3696,14 +3696,5 @@ const enUS = {
|
||||
selectFromSidebar: 'Select a plugin page from the sidebar',
|
||||
invalidPage: 'Invalid plugin page',
|
||||
},
|
||||
beta_banner: {
|
||||
message:
|
||||
'This environment is in Beta testing. Service stability is not guaranteed. Consider using',
|
||||
cloud_link: 'LangBot Cloud (dedicated environment)',
|
||||
or: 'or',
|
||||
oss_link: 'self-hosted open-source version',
|
||||
period: '.',
|
||||
dismiss: 'Dismiss',
|
||||
},
|
||||
};
|
||||
export default enUS;
|
||||
|
||||
@@ -3764,14 +3764,5 @@ const esES = {
|
||||
selectFromSidebar: 'Selecciona una página de plugin en la barra lateral',
|
||||
invalidPage: 'Página de plugin no válida',
|
||||
},
|
||||
beta_banner: {
|
||||
message:
|
||||
'Este entorno está en fase beta. La estabilidad del servicio no está garantizada. Considera usar',
|
||||
cloud_link: 'LangBot Cloud (entorno dedicado)',
|
||||
or: 'o',
|
||||
oss_link: 'la versión de código abierto autoalojada',
|
||||
period: '.',
|
||||
dismiss: 'Cerrar',
|
||||
},
|
||||
};
|
||||
export default esES;
|
||||
|
||||
@@ -3705,13 +3705,5 @@ const jaJP = {
|
||||
selectFromSidebar: 'サイドバーからプラグインページを選択してください',
|
||||
invalidPage: '無効なプラグインページ',
|
||||
},
|
||||
beta_banner: {
|
||||
message: 'この環境はベータテスト中です。サービスの安定性は保証されません。',
|
||||
cloud_link: 'LangBot Cloud(専用環境)',
|
||||
or: 'または',
|
||||
oss_link: 'セルフホスト版',
|
||||
period: 'をご検討ください。',
|
||||
dismiss: '閉じる',
|
||||
},
|
||||
};
|
||||
export default jaJP;
|
||||
|
||||
@@ -3722,14 +3722,5 @@ const ruRU = {
|
||||
selectFromSidebar: 'Выберите страницу плагина на боковой панели',
|
||||
invalidPage: 'Недопустимая страница плагина',
|
||||
},
|
||||
beta_banner: {
|
||||
message:
|
||||
'Эта среда находится в стадии бета-тестирования. Стабильность сервиса не гарантируется. Рекомендуем использовать',
|
||||
cloud_link: 'LangBot Cloud (выделенная среда)',
|
||||
or: 'или',
|
||||
oss_link: 'версию с открытым исходным кодом на своём сервере',
|
||||
period: '.',
|
||||
dismiss: 'Закрыть',
|
||||
},
|
||||
};
|
||||
export default ruRU;
|
||||
|
||||
@@ -3624,14 +3624,5 @@ const thTH = {
|
||||
selectFromSidebar: 'เลือกหน้าปลั๊กอินจากแถบด้านข้าง',
|
||||
invalidPage: 'หน้าปลั๊กอินไม่ถูกต้อง',
|
||||
},
|
||||
beta_banner: {
|
||||
message:
|
||||
'สภาพแวดล้อมนี้อยู่ระหว่างการทดสอบเบต้า ไม่รับประกันความเสถียรของบริการ โปรดพิจารณาใช้',
|
||||
cloud_link: 'LangBot Cloud (สภาพแวดล้อมเฉพาะ)',
|
||||
or: 'หรือ',
|
||||
oss_link: 'เวอร์ชันโอเพนซอร์สที่โฮสต์เอง',
|
||||
period: '',
|
||||
dismiss: 'ปิด',
|
||||
},
|
||||
};
|
||||
export default thTH;
|
||||
|
||||
@@ -3676,14 +3676,5 @@ const viVN = {
|
||||
selectFromSidebar: 'Chọn một trang plugin từ thanh bên',
|
||||
invalidPage: 'Trang plugin không hợp lệ',
|
||||
},
|
||||
beta_banner: {
|
||||
message:
|
||||
'Môi trường này đang trong giai đoạn thử nghiệm beta. Độ ổn định của dịch vụ không được đảm bảo. Hãy cân nhắc sử dụng',
|
||||
cloud_link: 'LangBot Cloud (môi trường riêng)',
|
||||
or: 'hoặc',
|
||||
oss_link: 'phiên bản mã nguồn mở tự lưu trữ',
|
||||
period: '.',
|
||||
dismiss: 'Đóng',
|
||||
},
|
||||
};
|
||||
export default viVN;
|
||||
|
||||
@@ -3487,13 +3487,5 @@ const zhHans = {
|
||||
selectFromSidebar: '从侧边栏选择一个插件页面',
|
||||
invalidPage: '无效的插件页面',
|
||||
},
|
||||
beta_banner: {
|
||||
message: '当前环境正在进行 Beta 测试,服务稳定性不受保证,建议优先选择',
|
||||
cloud_link: '独立环境版 LangBot Cloud',
|
||||
or: '或',
|
||||
oss_link: '自部署开源版本',
|
||||
period: '。',
|
||||
dismiss: '关闭横幅',
|
||||
},
|
||||
};
|
||||
export default zhHans;
|
||||
|
||||
@@ -3488,13 +3488,5 @@ const zhHant = {
|
||||
selectFromSidebar: '從側邊欄選擇一個插件頁面',
|
||||
invalidPage: '無效的插件頁面',
|
||||
},
|
||||
beta_banner: {
|
||||
message: '此環境正處於 Beta 測試階段,服務穩定性尚未保證。建議使用',
|
||||
cloud_link: 'LangBot Cloud(獨立環境)',
|
||||
or: '或',
|
||||
oss_link: '自行部署的開源版本',
|
||||
period: '。',
|
||||
dismiss: '關閉',
|
||||
},
|
||||
};
|
||||
export default zhHant;
|
||||
|
||||
@@ -22,6 +22,7 @@ new Function(
|
||||
)(m, m.exports);
|
||||
const {
|
||||
cloudPlanCheckoutURL,
|
||||
cloudSubscriptionManagementURL,
|
||||
cloudPlanPrice,
|
||||
localizedPlanText,
|
||||
fetchPublicCloudPlans,
|
||||
@@ -40,7 +41,7 @@ test('checkout links preserve the workspace and selection, never source credenti
|
||||
assert.equal(url.pathname, '/prefix/cloud');
|
||||
assert.equal(url.hash, '');
|
||||
assert.deepEqual(Object.fromEntries(url.searchParams), {
|
||||
environment: 'beta',
|
||||
environment: 'new',
|
||||
step: 'plan',
|
||||
workspace: 'workspace-1',
|
||||
plan: 'pro & team',
|
||||
@@ -179,3 +180,14 @@ test('plan currency follows the current UI language', () => {
|
||||
])
|
||||
assert.equal(m.exports.cloudPlanCurrencyForLanguage(locale), 'USD');
|
||||
});
|
||||
|
||||
test('subscription management links use the production environment', () => {
|
||||
const url = new URL(
|
||||
cloudSubscriptionManagementURL('https://space.example', 'w'),
|
||||
);
|
||||
assert.deepEqual(Object.fromEntries(url.searchParams), {
|
||||
environment: 'new',
|
||||
step: 'subscription',
|
||||
workspace: 'w',
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user