vojo/src/app/components/full-screen-intent-prompt/FullScreenIntentPrompt.tsx

143 lines
4.8 KiB
TypeScript

import React, { useCallback, useEffect, useState } from 'react';
import FocusTrap from 'focus-trap-react';
import {
Dialog,
Overlay,
OverlayCenter,
OverlayBackdrop,
Header,
config,
Box,
Text,
IconButton,
Icon,
Icons,
Button,
} from 'folds';
import { useTranslation } from 'react-i18next';
import { isNativePlatform } from '../../utils/capacitor';
import {
canUseFullScreenIntent,
openFullScreenIntentSettings,
} from '../../plugins/fullScreenIntent';
import { usePushEnabled } from '../../hooks/usePushNotifications';
import { stopPropagation } from '../../utils/keyboard';
// Mirrors PushPermissionPrompt's cooldown. 7 days is long enough that "not now"
// isn't nagging, short enough that a user who changes their mind doesn't have
// to dig through Settings to find the toggle.
const DISMISS_KEY = 'vojo_fsi_prompt_dismissed_at';
const DISMISS_COOLDOWN_MS = 7 * 24 * 60 * 60 * 1000;
const INITIAL_DELAY_MS = 1500;
const wasRecentlyDismissed = (): boolean => {
const raw = localStorage.getItem(DISMISS_KEY);
if (!raw) return false;
const ts = Number(raw);
if (!Number.isFinite(ts)) return false;
return Date.now() - ts < DISMISS_COOLDOWN_MS;
};
const markDismissed = (): void => {
localStorage.setItem(DISMISS_KEY, String(Date.now()));
};
export function FullScreenIntentPrompt() {
const { t } = useTranslation();
const pushEnabled = usePushEnabled();
const [visible, setVisible] = useState(false);
useEffect(() => {
// Only relevant when push is on — FSI wakeup only matters if we're actually
// going to show CallStyle notifications. Showing this before push is enabled
// would be out-of-context: user doesn't yet know what "incoming call screen"
// means in our UX. The PushPermissionPrompt comes first.
if (!isNativePlatform()) return undefined;
if (!pushEnabled) {
setVisible(false);
return undefined;
}
if (wasRecentlyDismissed()) return undefined;
let cancelled = false;
canUseFullScreenIntent().then((allowed) => {
if (cancelled) return;
if (allowed) return;
// Delay matches PushPermissionPrompt so the two never try to stack on
// top of each other at exact startup — PushPermissionPrompt goes first
// since its effect depends on `status === 'prompt'`; by the time that's
// resolved to 'granted' (and pushEnabled flips true), the PushPrompt is
// already gone and this one has room.
setTimeout(() => {
if (!cancelled) setVisible(true);
}, INITIAL_DELAY_MS);
});
return () => {
cancelled = true;
};
}, [pushEnabled]);
const handleLater = useCallback(() => {
markDismissed();
setVisible(false);
}, []);
const handleEnable = useCallback(() => {
// Opens Settings; the user returns to the app via the back button. We don't
// re-check canUseFullScreenIntent() on resume because the prompt will
// simply not re-show (7-day cooldown would apply if we markDismissed —
// we deliberately DO NOT mark dismissed here so if the user declines in
// Settings or forgets, the next startup still reminds them).
openFullScreenIntentSettings().catch(() => {
/* plugin missing — nothing to do */
});
setVisible(false);
}, []);
if (!visible) return null;
return (
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: handleLater,
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
<Dialog variant="Surface">
<Header
style={{
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
borderBottomWidth: config.borderWidth.B300,
}}
variant="Surface"
size="500"
>
<Box grow="Yes">
<Text size="H4">{t('Settings.fsi_prompt_title')}</Text>
</Box>
<IconButton size="300" onClick={handleLater} radii="300">
<Icon src={Icons.Cross} />
</IconButton>
</Header>
<Box style={{ padding: config.space.S400 }} direction="Column" gap="400">
<Text priority="400">{t('Settings.fsi_prompt_body')}</Text>
<Box direction="Row" gap="200" justifyContent="End">
<Button variant="Secondary" fill="Soft" onClick={handleLater}>
<Text size="B400">{t('Settings.fsi_prompt_later')}</Text>
</Button>
<Button variant="Primary" onClick={handleEnable}>
<Text size="B400">{t('Settings.fsi_prompt_open')}</Text>
</Button>
</Box>
</Box>
</Dialog>
</FocusTrap>
</OverlayCenter>
</Overlay>
);
}