- Updated CSS to utilize viewport variables for safe area insets and stable height, improving layout consistency across devices. - Introduced haptic feedback triggers in various components to enhance user interaction, mimicking native Telegram behavior. - Added functionality to detect Android performance class, minimizing animations on low-performance devices for better user experience. - Refactored components to incorporate new CSS classes for content safety and improved responsiveness.
87 lines
2.6 KiB
TypeScript
87 lines
2.6 KiB
TypeScript
/**
|
|
* Full-screen access denied view. Used when the user is not allowed (no initData / not localhost)
|
|
* or when API returns 403. Matches global-error and not-found layout: no extra chrome, one action.
|
|
*/
|
|
|
|
"use client";
|
|
|
|
import { useEffect } from "react";
|
|
import { getLang, translate } from "@/i18n/messages";
|
|
import { useAppStore } from "@/store/app-store";
|
|
import { triggerHapticLight } from "@/lib/telegram-haptic";
|
|
|
|
export interface AccessDeniedScreenProps {
|
|
/** Optional detail from API 403 response, shown below the hint. */
|
|
serverDetail?: string | null;
|
|
/** Primary button: reload (main page) or back/close (deep link). */
|
|
primaryAction: "reload" | "back";
|
|
/** Called when primaryAction is "back" (e.g. Back to calendar or Close). */
|
|
onBack?: () => void;
|
|
/** When true and primaryAction is "back", button label is "Close" instead of "Back to calendar". */
|
|
openedFromPin?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Full-screen access denied: title, hint, optional server detail, single action button.
|
|
* Calls setAppContentReady(true) on mount so Telegram receives ready().
|
|
*/
|
|
export function AccessDeniedScreen({
|
|
serverDetail,
|
|
primaryAction,
|
|
onBack,
|
|
openedFromPin = false,
|
|
}: AccessDeniedScreenProps) {
|
|
const lang = getLang();
|
|
const setAppContentReady = useAppStore((s) => s.setAppContentReady);
|
|
|
|
useEffect(() => {
|
|
setAppContentReady(true);
|
|
}, [setAppContentReady]);
|
|
|
|
const hasDetail = Boolean(serverDetail && String(serverDetail).trim());
|
|
|
|
const handleClick = () => {
|
|
triggerHapticLight();
|
|
if (primaryAction === "reload") {
|
|
if (typeof window !== "undefined") {
|
|
window.location.reload();
|
|
}
|
|
} else {
|
|
onBack?.();
|
|
}
|
|
};
|
|
|
|
const buttonLabel =
|
|
primaryAction === "reload"
|
|
? translate(lang, "access_denied.reload")
|
|
: openedFromPin
|
|
? translate(lang, "current_duty.close")
|
|
: translate(lang, "current_duty.back");
|
|
|
|
return (
|
|
<div
|
|
className="flex min-h-screen flex-col items-center justify-center gap-4 bg-background px-4 text-foreground"
|
|
role="alert"
|
|
>
|
|
<h1 className="text-xl font-semibold">
|
|
{translate(lang, "access_denied")}
|
|
</h1>
|
|
<p className="text-center text-muted-foreground">
|
|
{translate(lang, "access_denied.hint")}
|
|
</p>
|
|
{hasDetail && (
|
|
<p className="text-center text-sm text-muted-foreground">
|
|
{serverDetail}
|
|
</p>
|
|
)}
|
|
<button
|
|
type="button"
|
|
onClick={handleClick}
|
|
className="rounded-lg bg-primary px-4 py-2 text-primary-foreground hover:bg-primary/90"
|
|
>
|
|
{buttonLabel}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|