From 21d36bb53b849e17a9c3e46b065d64607e48188f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Falc=C3=B3n?= Date: Mon, 2 Feb 2026 08:28:52 +0100 Subject: [PATCH] feat: Show loading spinner on landing page when in PWA mode (#96) ## Summary - Prevents the landing page from briefly flashing before redirecting to `/dashboard` when accessing from an installed PWA - Detects standalone mode synchronously via a `useState` initializer so the spinner renders on the very first paint - Shows a centered loading spinner instead of the full landing page while the redirect happens ## Test plan - [x] Open the app as an installed PWA and verify a spinner shows instead of the landing page before redirecting to dashboard - [x] Open the landing page in a regular browser and verify the full landing page renders normally with no spinner --- resources/js/pages/welcome.tsx | 28 ++++++++++++++++++++++------ tests/TestCase.php | 6 +++--- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/resources/js/pages/welcome.tsx b/resources/js/pages/welcome.tsx index 8f0c2ec9..519e8a7f 100644 --- a/resources/js/pages/welcome.tsx +++ b/resources/js/pages/welcome.tsx @@ -1,6 +1,7 @@ import EncryptionVideoPlayer from '@/components/landing/encryption-video-player'; import Header from '@/components/partials/header'; import { Button } from '@/components/ui/button'; +import { Spinner } from '@/components/ui/spinner'; import { Tooltip, TooltipContent, @@ -28,7 +29,7 @@ import { TrendingUpIcon, ZapIcon, } from 'lucide-react'; -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; const LANDING_IMAGES = [ { @@ -160,13 +161,20 @@ export default function Welcome({ const planEntries = Object.entries(pricing.plans); const visitTrackedRef = useRef(false); - useEffect(() => { - const isStandalone = + const [isPwa] = useState(() => { + if (typeof window === 'undefined') { + return false; + } + + return ( window.matchMedia('(display-mode: standalone)').matches || ('standalone' in navigator && - (navigator as Navigator & { standalone: boolean }).standalone); + (navigator as Navigator & { standalone: boolean }).standalone) + ); + }); - if (isStandalone) { + useEffect(() => { + if (isPwa) { router.visit('/dashboard'); return; } @@ -178,7 +186,15 @@ export default function Welcome({ trackEvent(LEAD_FUNNEL_EVENT_UUID, { step: 'Visit', }); - }, []); + }, [isPwa]); + + if (isPwa) { + return ( +
+ +
+ ); + } return ( <> diff --git a/tests/TestCase.php b/tests/TestCase.php index 749504ca..99d48139 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -29,7 +29,7 @@ abstract class TestCase extends BaseTestCase */ protected function setupEncryptionKey($page, ?string $key = null, bool $reload = true): void { - $key = $key ?? $this->generateTestEncryptionKey(); + $key ??= $this->generateTestEncryptionKey(); try { $page->script("localStorage.setItem('encryption_key', ".json_encode($key).')'); } catch (\Exception) { @@ -39,7 +39,7 @@ abstract class TestCase extends BaseTestCase } if ($reload) { $currentUrl = $page->url(); - $page->navigate($currentUrl)->wait(1); + $page->navigate($currentUrl)->wait(1.5); } } @@ -48,7 +48,7 @@ abstract class TestCase extends BaseTestCase */ protected function visitWithEncryptionKey(string $url, ?string $key = null) { - $key = $key ?? $this->generateTestEncryptionKey(); + $key ??= $this->generateTestEncryptionKey(); $page = visit($url); $page->script("localStorage.setItem('encryption_key', ".json_encode($key).')'); $page->navigate($url)->wait(1);