mirror of
https://github.com/Xevion/dynamic-preauth.git
synced 2025-12-15 10:11:40 -06:00
feat(frontend): add mobile device detection and warning modal
Add mobile device detection to prevent confusion when users try to download desktop applications on mobile devices. The download button now displays "Download for Desktop" on mobile and shows a warning modal explaining that the executables are for desktop platforms only. The warning is shown once per session and can be acknowledged to proceed with viewing download options.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { Executable } from "@/components/useSocket";
|
||||
import { cn, withBackend } from "@/util";
|
||||
import MobileWarningModal from "@/components/MobileWarningModal";
|
||||
import { cn, isMobile, withBackend } from "@/util";
|
||||
import {
|
||||
Button,
|
||||
Menu,
|
||||
@@ -13,7 +14,9 @@ import {
|
||||
ChevronDownIcon,
|
||||
} from "@heroicons/react/16/solid";
|
||||
import { FaWindows, FaApple, FaLinux } from "react-icons/fa";
|
||||
import { useRef } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
const MOBILE_WARNING_KEY = "mobile-warning-acknowledged";
|
||||
|
||||
type DownloadButtonProps = {
|
||||
disabled?: boolean;
|
||||
@@ -70,15 +73,44 @@ export default function DownloadButton({
|
||||
buildLog,
|
||||
}: DownloadButtonProps) {
|
||||
const menuRef = useRef<HTMLButtonElement>(null);
|
||||
const [showMobileWarning, setShowMobileWarning] = useState(false);
|
||||
const [mobileAcknowledged, setMobileAcknowledged] = useState(() => {
|
||||
if (typeof window === "undefined") return false;
|
||||
return sessionStorage.getItem(MOBILE_WARNING_KEY) === "true";
|
||||
});
|
||||
|
||||
function getExecutable(id: string) {
|
||||
return executables?.find((e) => e.id.toLowerCase() === id.toLowerCase());
|
||||
}
|
||||
|
||||
const detectedPlatform = getSystemType();
|
||||
const mobile = isMobile();
|
||||
const detectedPlatform = mobile ? null : getSystemType();
|
||||
const platformExecutable = detectedPlatform ? getExecutable(detectedPlatform) : null;
|
||||
const canAutoDownload = platformExecutable != null;
|
||||
|
||||
function acknowledgeMobileWarning() {
|
||||
sessionStorage.setItem(MOBILE_WARNING_KEY, "true");
|
||||
setMobileAcknowledged(true);
|
||||
}
|
||||
|
||||
function handleMobileButtonClick() {
|
||||
if (!mobileAcknowledged) {
|
||||
setShowMobileWarning(true);
|
||||
} else {
|
||||
menuRef.current?.click();
|
||||
}
|
||||
}
|
||||
|
||||
function handleMobileWarningClose() {
|
||||
setShowMobileWarning(false);
|
||||
}
|
||||
|
||||
function handleMobileWarningContinue() {
|
||||
acknowledgeMobileWarning();
|
||||
setShowMobileWarning(false);
|
||||
menuRef.current?.click();
|
||||
}
|
||||
|
||||
async function handleDownload(id: string) {
|
||||
const executable = getExecutable(id);
|
||||
if (executable == null) {
|
||||
@@ -97,39 +129,65 @@ export default function DownloadButton({
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"[&>*]:py-1 overflow-clip transition-[background-color] text-sm/6 flex items-center shadow-inner align-middle text-white focus:outline-none data-[focus]:outline-1 data-[focus]:outline-white",
|
||||
!disabled
|
||||
? "divide-white/[0.2] shadow-white/10 bg-emerald-800 data-[hover]:bg-emerald-700 data-[open]:bg-emerald-700"
|
||||
: "divide-white/[0.1] shadow-white/5 animate-pulse-dark data-[hover]:bg-[#064e3b] cursor-wait",
|
||||
"rounded-md divide-x h-full rounded-l-md"
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
onClick={canAutoDownload ? handleDownloadAutomatic : undefined}
|
||||
suppressHydrationWarning
|
||||
disabled={disabled || !canAutoDownload}
|
||||
className={cn("pl-3 font-semibold pr-2.5", {
|
||||
"hover:bg-white/5 cursor-pointer": !disabled && canAutoDownload,
|
||||
"cursor-default": !canAutoDownload,
|
||||
})}
|
||||
<>
|
||||
<MobileWarningModal
|
||||
open={showMobileWarning}
|
||||
onClose={handleMobileWarningClose}
|
||||
onContinue={handleMobileWarningContinue}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"[&>*]:py-1 overflow-clip transition-[background-color] text-sm/6 flex items-center shadow-inner align-middle text-white focus:outline-none data-[focus]:outline-1 data-[focus]:outline-white",
|
||||
!disabled
|
||||
? "divide-white/[0.2] shadow-white/10 bg-emerald-800 data-[hover]:bg-emerald-700 data-[open]:bg-emerald-700"
|
||||
: "divide-white/[0.1] shadow-white/5 animate-pulse-dark data-[hover]:bg-[#064e3b] cursor-wait",
|
||||
"rounded-md divide-x h-full rounded-l-md"
|
||||
)}
|
||||
>
|
||||
{canAutoDownload && detectedPlatform
|
||||
? `Download for ${getPlatformDisplayName(detectedPlatform)}`
|
||||
: "Download"}
|
||||
</Button>
|
||||
<Menu>
|
||||
<MenuButton
|
||||
ref={menuRef}
|
||||
<Button
|
||||
onClick={
|
||||
mobile
|
||||
? handleMobileButtonClick
|
||||
: canAutoDownload
|
||||
? handleDownloadAutomatic
|
||||
: undefined
|
||||
}
|
||||
suppressHydrationWarning
|
||||
disabled={disabled}
|
||||
className={cn("pl-1.5 text-transparent min-h-8 pr-2", {
|
||||
"hover:bg-white/5": !disabled,
|
||||
disabled={disabled || (!mobile && !canAutoDownload)}
|
||||
className={cn("pl-3 font-semibold pr-2.5", {
|
||||
"hover:bg-white/5 cursor-pointer": !disabled && (mobile || canAutoDownload),
|
||||
"cursor-default": !mobile && !canAutoDownload,
|
||||
})}
|
||||
>
|
||||
<ChevronDownIcon className="size-4 fill-white/60" />
|
||||
</MenuButton>
|
||||
{mobile
|
||||
? "Download for Desktop"
|
||||
: canAutoDownload && detectedPlatform
|
||||
? `Download for ${getPlatformDisplayName(detectedPlatform)}`
|
||||
: "Download"}
|
||||
</Button>
|
||||
<Menu>
|
||||
{mobile && !mobileAcknowledged ? (
|
||||
<button
|
||||
onClick={handleMobileButtonClick}
|
||||
disabled={disabled}
|
||||
className={cn("pl-1.5 min-h-8 pr-2 py-1", {
|
||||
"hover:bg-white/5": !disabled,
|
||||
})}
|
||||
>
|
||||
<ChevronDownIcon className="size-4 fill-white/60" />
|
||||
</button>
|
||||
) : (
|
||||
<MenuButton
|
||||
ref={menuRef}
|
||||
suppressHydrationWarning
|
||||
disabled={disabled}
|
||||
className={cn("pl-1.5 text-transparent min-h-8 pr-2", {
|
||||
"hover:bg-white/5": !disabled,
|
||||
})}
|
||||
>
|
||||
<ChevronDownIcon className="size-4 fill-white/60" />
|
||||
</MenuButton>
|
||||
)}
|
||||
<MenuItems
|
||||
transition
|
||||
anchor="bottom end"
|
||||
@@ -169,5 +227,6 @@ export default function DownloadButton({
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
57
frontend/src/components/MobileWarningModal.tsx
Normal file
57
frontend/src/components/MobileWarningModal.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogBackdrop,
|
||||
DialogPanel,
|
||||
DialogTitle,
|
||||
} from "@headlessui/react";
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
type MobileWarningModalProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onContinue: () => void;
|
||||
};
|
||||
|
||||
export default function MobileWarningModal({
|
||||
open,
|
||||
onClose,
|
||||
onContinue,
|
||||
}: MobileWarningModalProps) {
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} className="relative z-50">
|
||||
<DialogBackdrop
|
||||
transition
|
||||
className="fixed inset-0 bg-black/60 backdrop-blur-sm transition-opacity duration-200 data-[closed]:opacity-0"
|
||||
/>
|
||||
|
||||
<div className="fixed inset-0 flex items-center justify-center p-4">
|
||||
<DialogPanel
|
||||
transition
|
||||
className="w-full max-w-sm rounded-xl border border-zinc-700 bg-zinc-900 p-5 shadow-xl transition-all duration-200 data-[closed]:scale-95 data-[closed]:opacity-0"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-amber-500/10">
|
||||
<ExclamationTriangleIcon className="h-5 w-5 text-amber-400" />
|
||||
</div>
|
||||
<DialogTitle className="text-lg font-semibold text-zinc-100">
|
||||
Heads up!
|
||||
</DialogTitle>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-zinc-300 leading-relaxed mb-4">
|
||||
These downloads are desktop applications for Windows, macOS, and
|
||||
Linux. They won't run on mobile devices, but you're welcome to
|
||||
download them to transfer to a computer later.
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={onContinue}
|
||||
className="w-full rounded-lg bg-emerald-700 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-emerald-600 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 focus:ring-offset-zinc-900"
|
||||
>
|
||||
Got it, continue
|
||||
</button>
|
||||
</DialogPanel>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,11 @@ export function os(): Platform | "other" {
|
||||
return "other";
|
||||
}
|
||||
|
||||
export function isMobile(): boolean {
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
return /android|iphone|ipad|ipod|webos|blackberry|windows phone/.test(ua);
|
||||
}
|
||||
|
||||
export function toHex(value: number): string {
|
||||
return "0x" + value.toString(16).toUpperCase();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user