diff --git a/apps/web/src/api/server-functions/getAppVersion.ts b/apps/web/src/api/server-functions/getAppVersion.ts
new file mode 100644
index 000000000..a76442972
--- /dev/null
+++ b/apps/web/src/api/server-functions/getAppVersion.ts
@@ -0,0 +1,8 @@
+import { createServerFn } from '@tanstack/react-start';
+
+export const getAppBuildId = createServerFn({ method: 'GET' }).handler(async () => {
+ return {
+ buildId: import.meta.env.VITE_BUILD_ID,
+ buildAt: import.meta.env.VITE_BUILD_AT,
+ };
+});
diff --git a/apps/web/src/components/ui/toaster/Toaster.tsx b/apps/web/src/components/ui/toaster/Toaster.tsx
index 68f5cd564..dd25bad3a 100644
--- a/apps/web/src/components/ui/toaster/Toaster.tsx
+++ b/apps/web/src/components/ui/toaster/Toaster.tsx
@@ -24,11 +24,11 @@ const Toaster = ({ ...props }: ToasterProps) => {
toastOptions={{
classNames: {
toast:
- 'group toast group-[.toaster]:bg-background! border !p-3 group-[.toaster]:text-foreground! group-[.toaster]:border-border! group-[.toaster]:shadow!',
+ 'group toast group-[.toaster]:bg-background border p-3 group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow',
description: 'group-[.toast]:text-gray-light',
actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton: 'group-[.toast]:bg-border group-[.toast]:text-foreground',
- icon: 'mx-0! !flex !justify-center',
+ icon: 'ml-0 mr-1 !flex !justify-center',
},
}}
{...props}
diff --git a/apps/web/src/context/AppVersion/useAppVersion.tsx b/apps/web/src/context/AppVersion/useAppVersion.tsx
new file mode 100644
index 000000000..461b1bad7
--- /dev/null
+++ b/apps/web/src/context/AppVersion/useAppVersion.tsx
@@ -0,0 +1,67 @@
+import { useQuery } from '@tanstack/react-query';
+import { useEffect, useState } from 'react';
+import { getAppBuildId } from '@/api/server-functions/getAppVersion';
+import { Text } from '@/components/ui/typography';
+import { useWindowFocus } from '@/hooks/useWindowFocus';
+import { useBusterNotifications } from '../BusterNotifications';
+
+const browserBuild = import.meta.env.VITE_BUILD_ID;
+
+export const useAppVersion = () => {
+ const { openInfoNotification } = useBusterNotifications();
+ const { data, refetch, isFetched } = useQuery({
+ queryKey: ['app-version'] as const,
+ queryFn: getAppBuildId,
+ refetchInterval: 20000, // 20 seconds
+ });
+ const isChanged = data?.buildId !== browserBuild && isFetched && browserBuild;
+
+ const reloadWindow = () => {
+ window.location.reload();
+ };
+
+ useWindowFocus(() => {
+ refetch().then(() => {
+ if (isChanged) {
+ reloadWindow();
+ }
+ });
+ });
+
+ useEffect(() => {
+ if (isChanged) {
+ openInfoNotification({
+ duration: Infinity,
+ title: 'New Version Available',
+ message: ,
+ dismissible: false,
+ className: 'min-w-[450px]',
+ action: {
+ label: 'Refresh',
+ onClick: () => {
+ reloadWindow();
+ },
+ },
+ });
+ }
+ }, [isChanged]);
+};
+
+const AppVersionMessage = () => {
+ // const [countdown, setCountdown] = useState(30);
+ // useEffect(() => {
+ // const interval = setInterval(() => {
+ // setCountdown((prev) => Math.max(prev - 1, 0));
+ // if (countdown === 0) {
+ // // window.location.reload();
+ // }
+ // }, 1000);
+ // return () => clearInterval(interval);
+ // }, []);
+
+ return (
+
+ A new version of the app is available. Please refresh the page to get the latest features.
+
+ );
+};
diff --git a/apps/web/src/context/BusterNotifications/BusterNotifications.tsx b/apps/web/src/context/BusterNotifications/BusterNotifications.tsx
index 5943f21e9..427c8dcf5 100644
--- a/apps/web/src/context/BusterNotifications/BusterNotifications.tsx
+++ b/apps/web/src/context/BusterNotifications/BusterNotifications.tsx
@@ -12,16 +12,16 @@ const ConfirmModal = lazy(() =>
import('@/components/ui/modal/ConfirmModal').then((mod) => ({ default: mod.ConfirmModal }))
);
-export interface NotificationProps {
+export type NotificationProps = {
type?: NotificationType;
title?: string;
- message?: string;
+ message?: string | React.ReactNode;
duration?: number;
action?: {
label: string;
onClick: () => void | (() => Promise);
};
-}
+} & ExternalToast;
const openNotification = (props: NotificationProps) => {
const { title, message, type } = props;
diff --git a/apps/web/src/context/Providers.tsx b/apps/web/src/context/Providers.tsx
index bf06d0471..31e313481 100644
--- a/apps/web/src/context/Providers.tsx
+++ b/apps/web/src/context/Providers.tsx
@@ -1,8 +1,8 @@
import type React from 'react';
import type { PropsWithChildren } from 'react';
import { BusterPosthogProvider } from '@/context/Posthog';
+import { useAppVersion } from './AppVersion/useAppVersion';
import { BusterStyleProvider } from './BusterStyles';
-
import {
SupabaseContextProvider,
type SupabaseContextType,
@@ -16,6 +16,8 @@ export const AppProviders: React.FC> = ({
children,
supabaseSession,
}) => {
+ useAppVersion();
+
return (
{children}
diff --git a/apps/web/src/routes/app/_app.tsx b/apps/web/src/routes/app/_app.tsx
index 5055f9dc4..ccc965200 100644
--- a/apps/web/src/routes/app/_app.tsx
+++ b/apps/web/src/routes/app/_app.tsx
@@ -28,6 +28,7 @@ export const Route = createFileRoute('/app/_app')({
},
component: () => {
const { initialLayout, defaultLayout } = Route.useLoaderData();
+
return (
{
const isLocalBuild = process.argv.includes('--local') || mode === 'development';
const target = isLocalBuild ? ('bun' as const) : ('vercel' as const);
+ // Generate a unique build ID for cache busting after deployments
+ const buildId = `build:${process.env.VERCEL_GIT_COMMIT_SHA?.slice(0, 8) || process.env.BUILD_ID || Date.now().toString()}`;
+ const buildAt = new Date().toISOString();
+
return {
server: { port: 3000 },
+ define: {
+ // Make the build ID available to the app for version tracking
+ 'import.meta.env.VITE_BUILD_ID': JSON.stringify(buildId),
+ 'import.meta.env.VITE_BUILD_AT': JSON.stringify(buildAt),
+ },
plugins: [
// this is the plugin that enables path aliases
viteTsConfigPaths({ projects: ['./tsconfig.json'] }),