From bbcd0db69ad4cfa0bd06c15d890d8c3529c30f09 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Mon, 22 Sep 2025 16:01:17 -0600 Subject: [PATCH] app version updated --- .../src/api/server-functions/getAppVersion.ts | 8 +++ .../web/src/components/ui/toaster/Toaster.tsx | 4 +- .../src/context/AppVersion/useAppVersion.tsx | 67 +++++++++++++++++++ .../BusterNotifications.tsx | 6 +- apps/web/src/context/Providers.tsx | 4 +- apps/web/src/routes/app/_app.tsx | 1 + apps/web/vite.config.ts | 9 +++ 7 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 apps/web/src/api/server-functions/getAppVersion.ts create mode 100644 apps/web/src/context/AppVersion/useAppVersion.tsx 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'] }),