diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index b487018..9801c63 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -400,13 +400,18 @@ export default function App() { useEffect(() => { if (IS_DEMO_MODE) { + const currentHashPath = typeof window !== 'undefined' + ? (window.location.hash || '').replace(/^#/, '').split('?')[0].split('#')[0] + : ''; + const normalizedCurrentHashPath = currentHashPath.replace(/^\/+/, '').replace(/\/+$/, ''); + const isDemoPublicSendRoute = /^send\/[^/]+(?:\/[^/]+)?$/i.test(normalizedCurrentHashPath); setDefaultKdfIterations(initialBootstrap.defaultKdfIterations); setJwtWarning(null); setSession(null); setProfile(null); setPhase('login'); setUnlockPreparing(false); - if (location !== '/login') navigate('/login'); + if (!isDemoPublicSendRoute && location !== '/login') navigate('/login'); return; } @@ -956,6 +961,7 @@ export default function App() { }); useEffect(() => { + if (IS_DEMO_MODE) return; if (phase !== 'app' || !vaultInitialDecryptDone) return; void preloadAuthenticatedWorkspace(isAdmin); }, [phase, vaultInitialDecryptDone, isAdmin]); diff --git a/webapp/src/components/PublicSendPage.tsx b/webapp/src/components/PublicSendPage.tsx index 288c753..b323692 100644 --- a/webapp/src/components/PublicSendPage.tsx +++ b/webapp/src/components/PublicSendPage.tsx @@ -87,12 +87,13 @@ function parsePublicSendData(value: unknown): PublicSendData | null { } export default function PublicSendPage(props: PublicSendPageProps) { - const [loading, setLoading] = useState(true); + const initialDemoSend = IS_DEMO_MODE ? getDemoPublicSend(props.accessId) : null; + const [loading, setLoading] = useState(!IS_DEMO_MODE); const [password, setPassword] = useState(''); const [needPassword, setNeedPassword] = useState(false); const [error, setError] = useState(''); - const [notFound, setNotFound] = useState(false); - const [sendData, setSendData] = useState(null); + const [notFound, setNotFound] = useState(IS_DEMO_MODE && !initialDemoSend); + const [sendData, setSendData] = useState(initialDemoSend); const [busy, setBusy] = useState(false); const [downloadPercent, setDownloadPercent] = useState(null); const loadRequestRef = useRef(0); @@ -201,6 +202,15 @@ export default function PublicSendPage(props: PublicSendPageProps) { } useEffect(() => { + if (IS_DEMO_MODE) { + const demoSend = getDemoPublicSend(props.accessId); + setSendData(demoSend); + setNotFound(!demoSend); + setNeedPassword(false); + setError(''); + setLoading(false); + return; + } void loadSend(); return () => { loadAbortRef.current?.abort(); diff --git a/webapp/src/components/SendsPage.tsx b/webapp/src/components/SendsPage.tsx index e00f3c1..3324bb9 100644 --- a/webapp/src/components/SendsPage.tsx +++ b/webapp/src/components/SendsPage.tsx @@ -224,8 +224,17 @@ export default function SendsPage(props: SendsPageProps) { } } + function getAccessUrl(send: Send): string { + const rawUrl = send.shareUrl || `/send/${send.accessId}`; + if (/^https?:\/\//i.test(rawUrl)) return rawUrl; + if (rawUrl.startsWith('/#/')) return `${window.location.origin}${rawUrl}`; + if (rawUrl.startsWith('#/')) return `${window.location.origin}/${rawUrl}`; + if (rawUrl.startsWith('/')) return `${window.location.origin}/#${rawUrl}`; + return `${window.location.origin}/#/${rawUrl.replace(/^\/+/, '')}`; + } + function copyAccessUrl(send: Send): void { - const url = send.shareUrl || `${window.location.origin}/#/send/${send.accessId}`; + const url = getAccessUrl(send); void copyTextToClipboard(url, { successMessage: t('txt_link_copied') }); } diff --git a/webapp/src/components/vault/WebsiteIcon.tsx b/webapp/src/components/vault/WebsiteIcon.tsx index 76bf69c..e15f731 100644 --- a/webapp/src/components/vault/WebsiteIcon.tsx +++ b/webapp/src/components/vault/WebsiteIcon.tsx @@ -8,6 +8,7 @@ import { preloadWebsiteIcon, subscribeWebsiteIconStatus, } from '@/lib/website-icon-cache'; +import { demoBrandIconUrl } from '@/lib/demo-brand-icons'; import { firstCipherUri, hostFromUri, websiteIconUrl } from '@/lib/website-utils'; const ICON_LOAD_ROOT_MARGIN = '180px 0px'; @@ -25,21 +26,7 @@ export default function WebsiteIcon(props: WebsiteIconProps) { const [shouldLoad, setShouldLoad] = useState(() => (host ? getWebsiteIconStatus(host) === 'loaded' : true)); const [status, setStatus] = useState(() => (host ? getWebsiteIconStatus(host) : 'idle')); const [imageUrl, setImageUrl] = useState(() => (host ? getWebsiteIconImageUrl(host) : '')); - const [demoIconUrl, setDemoIconUrl] = useState(''); - - useEffect(() => { - if (!SHOULD_LOAD_DEMO_BRAND_ICONS || !host) { - setDemoIconUrl(''); - return; - } - let disposed = false; - void import('@/lib/demo-brand-icons').then(({ demoBrandIconUrl }) => { - if (!disposed) setDemoIconUrl(demoBrandIconUrl(host)); - }); - return () => { - disposed = true; - }; - }, [host]); + const demoIconUrl = SHOULD_LOAD_DEMO_BRAND_ICONS && host ? demoBrandIconUrl(host) : ''; useEffect(() => { if (!host) { @@ -88,6 +75,7 @@ export default function WebsiteIcon(props: WebsiteIconProps) { }, [host, shouldLoad, status]); useEffect(() => { + if (SHOULD_LOAD_DEMO_BRAND_ICONS) return; if (demoIconUrl) return; if (!host || !src || !shouldLoad || status === 'loaded' || status === 'error') return; let disposed = false; diff --git a/webapp/src/lib/demo.ts b/webapp/src/lib/demo.ts index c74227e..f43dd86 100644 --- a/webapp/src/lib/demo.ts +++ b/webapp/src/lib/demo.ts @@ -422,7 +422,7 @@ export const DEMO_SENDS: Send[] = [ deletionDate: '2026-05-18T08:00:00.000Z', expirationDate: null, revisionDate: DEMO_NOW, - shareUrl: '/send/demo-note/demo-key', + shareUrl: '/#/send/demo-note/demo-key', }, { id: 'send-demo-file', @@ -438,7 +438,7 @@ export const DEMO_SENDS: Send[] = [ deletionDate: '2026-05-11T08:00:00.000Z', expirationDate: '2026-05-08T08:00:00.000Z', revisionDate: DEMO_NOW, - shareUrl: '/send/demo-file/demo-key', + shareUrl: '/#/send/demo-file/demo-key', file: { id: 'send-file-001', fileName: 'design-handoff.zip', @@ -730,7 +730,7 @@ function sendFromDraft(draft: SendDraft, current?: Send | null): Send { deletionDate: new Date(Date.now() + deletionDays * 86400_000).toISOString(), expirationDate: expirationDays > 0 ? new Date(Date.now() + expirationDays * 86400_000).toISOString() : null, revisionDate: now, - shareUrl: current?.shareUrl || (isFile ? '/send/demo-file/demo-key' : '/send/demo-note/demo-key'), + shareUrl: current?.shareUrl || (isFile ? '/#/send/demo-file/demo-key' : '/#/send/demo-note/demo-key'), file: isFile ? { id: current?.file?.id || createDemoId('send-file'), fileName, diff --git a/webapp/vite.config.ts b/webapp/vite.config.ts index b07ec1f..5d2226b 100644 --- a/webapp/vite.config.ts +++ b/webapp/vite.config.ts @@ -60,16 +60,19 @@ export default defineConfig(({ mode }) => { } if ( - normalized.includes('/src/components/ImportPage.tsx') || - normalized.includes('/src/lib/import-') || - normalized.includes('/src/lib/export-formats.ts') || - normalized.includes('/src/components/SendsPage.tsx') || - normalized.includes('/src/components/TotpCodesPage.tsx') || - normalized.includes('/src/components/BackupCenterPage.tsx') || - normalized.includes('/src/components/backup-center/') || - normalized.includes('/src/components/SettingsPage.tsx') || - normalized.includes('/src/components/SecurityDevicesPage.tsx') || - normalized.includes('/src/components/AdminPage.tsx') + !isDemo && + ( + normalized.includes('/src/components/ImportPage.tsx') || + normalized.includes('/src/lib/import-') || + normalized.includes('/src/lib/export-formats.ts') || + normalized.includes('/src/components/SendsPage.tsx') || + normalized.includes('/src/components/TotpCodesPage.tsx') || + normalized.includes('/src/components/BackupCenterPage.tsx') || + normalized.includes('/src/components/backup-center/') || + normalized.includes('/src/components/SettingsPage.tsx') || + normalized.includes('/src/components/SecurityDevicesPage.tsx') || + normalized.includes('/src/components/AdminPage.tsx') + ) ) { return 'workspace-suite'; }