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 (
{step}
);
})}
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 (