diff --git a/platforms/blabsy/src/components/layout/common-layout.tsx b/platforms/blabsy/src/components/layout/common-layout.tsx index 41dcdc101..f345f986a 100644 --- a/platforms/blabsy/src/components/layout/common-layout.tsx +++ b/platforms/blabsy/src/components/layout/common-layout.tsx @@ -2,77 +2,139 @@ import { useRequireAuth } from '@lib/hooks/useRequireAuth'; import { Aside } from '@components/aside/aside'; import { Suggestions } from '@components/aside/suggestions'; import { Placeholder } from '@components/common/placeholder'; -import { type ReactNode, useState } from 'react'; +import { type ReactNode, useState, useEffect, useRef } from 'react'; import { Modal } from '@components/modal/modal'; import { Button } from '@components/ui/button'; -import { useAuth } from '@lib/context/auth-context'; export type LayoutProps = { children: ReactNode; }; +const DISCLAIMER_KEY = 'blabsy-disclaimer-accepted'; + +// Safe localStorage access for restricted environments +const safeGetItem = (key: string): string | null => { + try { + return localStorage.getItem(key); + } catch { + return null; + } +}; + +const safeSetItem = (key: string, value: string): void => { + try { + localStorage.setItem(key, value); + } catch { + // Silently fail in restricted environments + } +}; + export function ProtectedLayout({ children }: LayoutProps): JSX.Element { const user = useRequireAuth(); - const { signOut } = useAuth(); const [disclaimerAccepted, setDisclaimerAccepted] = useState(false); + const [disclaimerChecked, setDisclaimerChecked] = useState(false); + const [showHint, setShowHint] = useState(false); + const [isPulsing, setIsPulsing] = useState(false); + + useEffect(() => { + let accepted = false; + try { + accepted = localStorage.getItem(DISCLAIMER_KEY) === 'true'; + } catch { + // Storage may be unavailable; fall back to session-only acceptance. + } + setDisclaimerAccepted(accepted); + setDisclaimerChecked(true); + }, []); + + const pulseTimeoutRef = useRef | null>(null); + const handleOutsideClick = () => { + setIsPulsing(true); + setShowHint(true); + if (pulseTimeoutRef.current) clearTimeout(pulseTimeoutRef.current); + pulseTimeoutRef.current = setTimeout(() => setIsPulsing(false), 400); + }; if (!user) return ; + if (!disclaimerChecked) return <>; + if (disclaimerAccepted) return <>{children}; return ( <> {children} - {!disclaimerAccepted ? ( - signOut()} - className='max-w-lg mx-auto mt-24' - modalClassName='bg-black backdrop-blur-md p-6 rounded-lg flex flex-col gap-2' + + +

+ Disclaimer from MetaState Foundation +

+

⚠️ Please note:

+

+ Blabsy is a functional prototype, intended to + showcase interoperability and core concepts of + the W3DS ecosystem. +

+

+ It is not a production-grade platform and may + lack full reliability, performance, and security + guarantees. +

+

+ We strongly recommend that you avoid sharing{' '} + sensitive or private content, and kindly ask for + your understanding regarding any bugs, incomplete + features, or unexpected behaviours. +

+

+ The app is still in development, so we kindly ask for + your understanding regarding any potential issues. If + you experience issues or have feedback, feel free to + contact us at: +

+ -

- Disclaimer from MetaState Foundation -

-

⚠️ Please note:

-

- Blabsy is a functional prototype, intended to - showcase interoperability and core concepts of - the W3DS ecosystem. -

-

- It is not a production-grade platform and may - lack full reliability, performance, and security - guarantees. -

-

- We strongly recommend that you avoid sharing{' '} - sensitive or private content, and kindly ask for - your understanding regarding any bugs, incomplete - features, or unexpected behaviours. -

-

- The app is still in development, so we kindly ask for - your understanding regarding any potential issues. If - you experience issues or have feedback, feel free to - contact us at: -

-
- info@metastate.foundation - + info@metastate.foundation + +
+ {showHint && ( +
+ 💡 You must accept the disclaimer to continue. This will only appear once. +
+ )} - - ) : ( - <> - )} +
+
); } diff --git a/platforms/eVoting/src/app/(app)/layout.tsx b/platforms/eVoting/src/app/(app)/layout.tsx index eaaf9d047..6ac642d6d 100644 --- a/platforms/eVoting/src/app/(app)/layout.tsx +++ b/platforms/eVoting/src/app/(app)/layout.tsx @@ -1,6 +1,5 @@ "use client"; import Navigation from "@/components/navigation"; -import { useAuth } from "@/lib/auth-context"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; @@ -12,7 +11,33 @@ import { DialogDescription, DialogFooter, } from "@/components/ui/dialog"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { ProtectedRoute } from "@/components/auth/protected-route"; +import { cn } from "@/lib/utils"; + +const DISCLAIMER_KEY = "evoting-disclaimer-accepted"; + +// Safe localStorage access for restricted environments +const safeGetItem = (key: string): string | null => { + try { + return localStorage.getItem(key); + } catch { + return null; + } +}; + +const safeSetItem = (key: string, value: string): void => { + try { + localStorage.setItem(key, value); + } catch { + // Silently fail in restricted environments + } +}; // Deeplink handling for reveal functionality declare global { @@ -26,19 +51,34 @@ export default function AppLayout({ }: Readonly<{ children: React.ReactNode; }>) { - const { logout } = useAuth(); const [disclaimerAccepted, setDisclaimerAccepted] = useState(false); + const [isPulsing, setIsPulsing] = useState(false); + const [showHint, setShowHint] = useState(false); const router = useRouter(); + useEffect(() => { + const accepted = safeGetItem(DISCLAIMER_KEY) === "true"; + if (accepted) { + setDisclaimerAccepted(true); + } + }, []); + + const handleInteractOutside = (e: Event) => { + e.preventDefault(); + setIsPulsing(true); + setShowHint(true); + setTimeout(() => setIsPulsing(false), 400); + }; + // Handle deeplink reveal requests useEffect(() => { const handleDeepLinkReveal = (event: CustomEvent<{ pollId: string }>) => { console.log("🔍 Deep link reveal request received:", event.detail); const { pollId } = event.detail; - + // Navigate to the poll page to show reveal interface router.push(`/${pollId}`); - + // Store the reveal request in sessionStorage for the poll page to pick up sessionStorage.setItem("revealRequest", JSON.stringify({ pollId, timestamp: Date.now() })); }; @@ -52,77 +92,112 @@ export default function AppLayout({ }; }, [router]); + if (disclaimerAccepted) { + return ( + + <> + + {children} + + + ); + } + return ( <> {children} - {!disclaimerAccepted ? ( - - logout()} - > - - - Disclaimer from MetaState Foundation - - -
-

⚠️ Please note:

-

- eVoting is a functional prototype - , intended to showcase{" "} - interoperability and core - concepts of the W3DS ecosystem. -

-

- - It is not a production-grade - platform - {" "} - and may lack full reliability, - performance, and security guarantees. -

-

- We strongly recommend that you - avoid sharing{" "} - sensitive or private content, and - kindly ask for your understanding - regarding any bugs, incomplete features, - or unexpected behaviours. -

-

- The app is still in development, so we - kindly ask for your understanding - regarding any potential issues. If you - experience issues or have feedback, feel - free to contact us at: -

- + + + + + Disclaimer from MetaState Foundation + + + + + + + + + +
-
-
- - - -
-
- ) : ( - <> - )} + I Understand + + + +

+ You must accept the disclaimer to continue. This will only appear once. +

+
+ + + + +
); diff --git a/platforms/eVoting/src/components/ui/dialog.tsx b/platforms/eVoting/src/components/ui/dialog.tsx index 248380ea1..6382ca292 100644 --- a/platforms/eVoting/src/components/ui/dialog.tsx +++ b/platforms/eVoting/src/components/ui/dialog.tsx @@ -31,8 +31,10 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; const DialogContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( + React.ComponentPropsWithoutRef & { + hideCloseButton?: boolean + } +>(({ className, children, hideCloseButton, ...props }, ref) => ( {children} + {!hideCloseButton && ( + + + Close + + )} )); diff --git a/platforms/group-charter-manager/src/components/disclaimer-modal.tsx b/platforms/group-charter-manager/src/components/disclaimer-modal.tsx index 18fa3aaa5..da337869a 100644 --- a/platforms/group-charter-manager/src/components/disclaimer-modal.tsx +++ b/platforms/group-charter-manager/src/components/disclaimer-modal.tsx @@ -9,80 +9,145 @@ import { DialogDescription, DialogFooter, } from "@/components/ui/dialog"; -import { useState } from "react"; -import { useAuth } from "@/components/auth/auth-provider"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { useState, useEffect } from "react"; +import { cn } from "@/lib/utils"; + +const DISCLAIMER_KEY = "group-charter-disclaimer-accepted"; + +// Safe localStorage access for restricted environments +const safeGetItem = (key: string): string | null => { + try { + return localStorage.getItem(key); + } catch { + return null; + } +}; + +const safeSetItem = (key: string, value: string): void => { + try { + localStorage.setItem(key, value); + } catch { + // Silently fail in restricted environments + } +}; export default function DisclaimerModal() { - const { logout } = useAuth(); - const [disclaimerAccepted, setDisclaimerAccepted] = useState(false); + const [disclaimerAccepted, setDisclaimerAccepted] = useState(true); // Start as true to prevent flash + const [showHint, setShowHint] = useState(false); + const [isPulsing, setIsPulsing] = useState(false); + + useEffect(() => { + // Check if disclaimer was previously accepted + const accepted = safeGetItem(DISCLAIMER_KEY) === "true"; + setDisclaimerAccepted(accepted); + }, []); + + const handleInteractOutside = (e: Event) => { + e.preventDefault(); + setIsPulsing(true); + setShowHint(true); + setTimeout(() => setIsPulsing(false), 400); + }; + + if (disclaimerAccepted) return null; + return ( - <> - {!disclaimerAccepted ? ( - - logout()} - > - - - Disclaimer from MetaState Foundation - - -
-

⚠️ Please note:

-

- Group Charter is a{" "} - functional prototype, intended to - showcase interoperability and - core concepts of the W3DS ecosystem. -

-

- - It is not a production-grade - platform - {" "} - and may lack full reliability, - performance, and security guarantees. -

-

- We strongly recommend that you - avoid sharing{" "} - sensitive or private content, and - kindly ask for your understanding - regarding any bugs, incomplete features, - or unexpected behaviours. -

-

- The app is still in development, so we - kindly ask for your understanding - regarding any potential issues. If you - experience issues or have feedback, feel - free to contact us at: -

- - info@metastate.foundation - -
-
-
- - - -
-
- ) : ( - <> - )} - + info@metastate.foundation + + + + + + + + + + + +

+ You must accept the disclaimer to continue. This will only appear once. +

+
+
+
+
+ + ); } diff --git a/platforms/group-charter-manager/src/components/ui/dialog.tsx b/platforms/group-charter-manager/src/components/ui/dialog.tsx index f38593bdc..62dbb94aa 100644 --- a/platforms/group-charter-manager/src/components/ui/dialog.tsx +++ b/platforms/group-charter-manager/src/components/ui/dialog.tsx @@ -31,8 +31,10 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName const DialogContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( + React.ComponentPropsWithoutRef & { + hideCloseButton?: boolean + } +>(({ className, children, hideCloseButton, ...props }, ref) => ( {children} - - - Close - + {!hideCloseButton && ( + + + Close + + )} )) diff --git a/platforms/pictique/src/lib/ui/Modal/Modal.svelte b/platforms/pictique/src/lib/ui/Modal/Modal.svelte index 9fea3b835..00e66339f 100644 --- a/platforms/pictique/src/lib/ui/Modal/Modal.svelte +++ b/platforms/pictique/src/lib/ui/Modal/Modal.svelte @@ -4,10 +4,11 @@ interface IModalProps { open: boolean; onclose?: () => void; + onClickOutside?: (modal: HTMLDialogElement) => void; children?: Snippet; } - const { open, onclose, children }: IModalProps = $props(); + const { open, onclose, onClickOutside, children }: IModalProps = $props(); let modal: HTMLDialogElement | null = $state(null); @@ -20,8 +21,14 @@ rect.left <= event.clientX && event.clientX <= rect.left + rect.width; if (!isInDialog) { - modal.close(); - onclose?.(); + event.stopPropagation(); + if (onClickOutside) { + onClickOutside(modal); + } else { + // Default behavior: close the modal + modal.close(); + onclose?.(); + } } }; diff --git a/platforms/pictique/src/routes/(protected)/+layout.svelte b/platforms/pictique/src/routes/(protected)/+layout.svelte index 73a5043ff..ca6cfc5bd 100644 --- a/platforms/pictique/src/routes/(protected)/+layout.svelte +++ b/platforms/pictique/src/routes/(protected)/+layout.svelte @@ -8,7 +8,6 @@ import type { userProfile } from '$lib/types'; import { Button, Modal } from '$lib/ui'; import { apiClient, getAuthId, getAuthToken } from '$lib/utils'; - import { removeAuthId, removeAuthToken } from '$lib/utils'; import type { AxiosError } from 'axios'; import { onMount } from 'svelte'; @@ -18,6 +17,26 @@ let profile = $state(null); let confirmedDisclaimer = $state(false); + let showHint = $state(false); + + const DISCLAIMER_KEY = 'pictique-disclaimer-accepted'; + + // Safe localStorage access for restricted environments + const safeGetItem = (key: string): string | null => { + try { + return localStorage.getItem(key); + } catch { + return null; + } + }; + + const safeSetItem = (key: string, value: string): void => { + try { + localStorage.setItem(key, value); + } catch { + // Silently fail in restricted environments + } + }; async function fetchProfile() { ownerId = getAuthId(); @@ -37,7 +56,14 @@ } } - onMount(fetchProfile); + onMount(() => { + fetchProfile(); + const accepted = safeGetItem(DISCLAIMER_KEY) === 'true'; + if (accepted) { + confirmedDisclaimer = true; + closeDisclaimerModal(); + } + });
@@ -57,11 +83,22 @@ { + onClickOutside={(modal) => { if (!confirmedDisclaimer) { - removeAuthToken(); - removeAuthId(); - goto('/auth'); + showHint = true; + modal.animate( + [ + { transform: 'scale(1)' }, + { transform: 'scale(1.025)' }, + { transform: 'scale(1)' }, + { transform: 'scale(1.0125)' }, + { transform: 'scale(1)' } + ], + { + duration: 250, + easing: 'ease-in-out' + } + ); } }} > @@ -91,11 +128,20 @@ + {#if showHint} +

+ 💡 You must accept the disclaimer to continue. This will only appear once. +

+ {/if}