import { Button, DialogDisclosure, DialogDismiss } from "@ariakit/react"; import { useCallback, useMemo, useState, type ReactNode } from "react"; import { number } from "superstruct"; import useImmutableSWR from "swr/immutable"; import { Link } from "wouter"; import { useAppConfig, useClient } from "../AppConfig/AppConfig.tsx"; import { Failure, Pending, Success } from "../async-op.ts"; import { interactiveText } from "../common.css.ts"; import { createSafeStorage } from "../createSafeStorage.ts"; import { DialogTrigger } from "../DialogTrigger/index.tsx"; import { LetterheadFooter, LetterheadHeading, LetterheadParagraph, } from "../Letterhead/index.tsx"; import { button } from "../Letterhead/style.css.ts"; import { LoadingIndicator } from "../LoadingIndicator.tsx"; import { ModalDialog } from "../ModalDialog/index.tsx"; import { swrResponseToResult } from "../result/swr.ts"; import { useCurrentUser } from "../store/index.tsx"; import type { FailurePayload } from "../types.ts"; import { useIsInstalled } from "../use-is-installed.ts"; import addToDockUrl from "./addToDock.svg"; import addToHomeScreenUrl from "./addToHomeScreen.svg"; import safariUrl from "./safari.svg"; import shareIconUrl from "./shareIcon.svg"; import * as css from "./style.css.ts"; function InstallAppDialog({ isMobile }: { isMobile?: boolean }) { const config = useAppConfig(); const shareIcon = ; const addIcon = ; const addToDockIcon = ; const steps = [ <> Open the Share menu {shareIcon} , isMobile ? ( <> Tap Add to Home Screen {addIcon} ) : ( <> Click Add to Dock {addToDockIcon} ), isMobile ? ( <> Tap Add in the top right corner ) : ( <> Click Add ), ]; return ( Install {config.app.name} To install this app, follow these 3 steps:
    {steps.map((step, index) => { return (
  1. {step}
  2. ); })}
Dismiss
); } function SafariPrompt(props: { onDismiss: () => void; isMobile?: boolean }) { const { hrefs } = useAppConfig(); const delete_your_local_data = delete your local data; const install_the_app = install the app; const creating_an_account = creating an account; return (
Heads up Safari users!
Safari — unlike other browsers — will {delete_your_local_data} after 7 days of inactivity, unless you have installed the app. To prevent accidental data loss, you can either {install_the_app}, make sure all your data is safely backed up by {creating_an_account}, or use a different browser.
Install App Create Account
); } function useUserAgent(props: { performFetch: boolean }) { const client = useClient(); const swr = useImmutableSWR( props.performFetch ? "/ua" : null, useCallback(() => client.userAgent(), []), ); return swrResponseToResult(swr); } const storage = createSafeStorage({ safariInstallPromptDismissedAt: number() }); function useInstallPromptState() { const [dismissedAt, setState] = useState(() => storage.getItem("safariInstallPromptDismissedAt"), ); return { dismissedAt, setDismissed: useCallback(() => { const timestamp = Date.now(); setState(timestamp); storage.setItem("safariInstallPromptDismissedAt", timestamp); }, []), }; } type SafariCheckResult = | Success<{ showPrompt: boolean; isMobile?: boolean }> | Failure | Pending; function useSafariCheck({ performCheck }: { performCheck: boolean }) { const isAuthenticated = !!useCurrentUser(); const isInstalled = useIsInstalled(); const { dismissedAt, setDismissed } = useInstallPromptState(); const installedOrDismissed = isInstalled || !!dismissedAt; const userAgentResult = useUserAgent({ performFetch: performCheck && !isAuthenticated && !installedOrDismissed, }); const result = useMemo((): SafariCheckResult => { return isAuthenticated || installedOrDismissed || !performCheck ? // If the safari prompt was previously dismissed, the app is already // installed, or check was disabled explicitly, we skip the prompt. new Success({ showPrompt: false }) : // Otherwise, we want to check whether the name includes Safari (could be // Safari or Safari Mobile), and we also report whether this is a mobile // device to allow for more targeted install instructions for the user. userAgentResult.mapSuccess((value) => { return { showPrompt: value.browser?.name?.includes("Safari") ?? false, isMobile: value.device?.type === "mobile", }; }); }, [installedOrDismissed, userAgentResult, performCheck, isAuthenticated]); return { result, setDismissed }; } /** * Checks whether the browser which is running the app is Safari/Safari Mobile * and warns the user that their data might be deleted due to inactivity. * * The warning will be shown if: * * - The check has not been explicitly disabled via `performCheck: false` * - The warning has not been previously dismissed (as reported by localStorage) * - The app is not installed (Safari behaves differently in that case) * - The user is not already logged in (as data will be safely backed up that way) */ export function SafariCheck(props: { children: ReactNode; /** * Optionally opt out of the check. * * This can be useful in cases where, e.g. the user already has some content * in the app and we want to avoid checking the user agent, but it is * impractical to entirely avoid rendering this component. */ performCheck?: boolean; }) { const { result, setDismissed } = useSafariCheck({ performCheck: props.performCheck ?? true, }); if (result.isPending) { return (
{/* Intentionally not using Letterhead Paragraph to pick up the default font size and style. */}

Checking compatibility...

); } // Get the success value. Note that if the UA check is a failure we don't // want to bother the user with any failure messages. In that case, we just // keep rolling as if the showPrompt was false. const value = result.valueOrNull(); if (value?.showPrompt) { return (
); } // We're all good, render children. return <>{props.children}; }