mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-12 23:31:02 +00:00
b7951baf23
- RTL: the measured pills and centred controls use logical forms or a marked line - i18n: the two native widget numbers are positions, not counts (NOT_PLURAL) - Types: styleOverride on the lazy GL map, the location and shared-file payloads, the capabilities fallback, the transport sheet's stay metadata - ESLint: latest-value refs move to layout effects, usePresence keeps the last open state in state, the booking import opens clean with a shared document kept in the list (the reset effect used to clear it again) - Layers: the phone import sheet lives under mobile/ and reads through tripRepo; the offline host check moves to utils/tileUrl - Sizes: plugin layer geometry, hardNavigate and the exit animations get modules of their own - Tests follow the status radios, the stay chip overview, the dock + button for the first list and the field token; store registry lists the new stores
86 lines
3.2 KiB
TypeScript
86 lines
3.2 KiB
TypeScript
/**
|
|
* AppLockGate: covers TREK until Face ID, Touch ID or the passcode unlocks it,
|
|
* when the app lock is on (native/appLock.ts). It locks at launch and again
|
|
* after the app spent more than half a minute in the background, and asks at
|
|
* once rather than waiting for a tap; the button is there for a second try.
|
|
* Rendered once in App; outside the app, or with the lock off, it is nothing.
|
|
*/
|
|
import { App } from '@capacitor/app';
|
|
import { Lock } from 'lucide-react';
|
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
import { useTranslation } from '../../i18n';
|
|
import { RELOCK_AFTER_MS, isAppLockEnabled, unlockApp } from '../../native/appLock';
|
|
import { useNativeCapabilities } from '../../native/capabilities';
|
|
|
|
export default function AppLockGate(): React.ReactElement | null {
|
|
const { t } = useTranslation();
|
|
// Re-read once the binary has said what it can do.
|
|
const { caps } = useNativeCapabilities();
|
|
const [locked, setLocked] = useState(false);
|
|
const asking = useRef(false);
|
|
const backgroundSince = useRef<number | null>(null);
|
|
|
|
const ask = useCallback(async () => {
|
|
if (asking.current) return;
|
|
asking.current = true;
|
|
const ok = await unlockApp(t('native.lock.reason'));
|
|
asking.current = false;
|
|
if (ok) setLocked(false);
|
|
}, [t]);
|
|
|
|
// At launch, once the capabilities are known: locked in the same render the
|
|
// capabilities arrive in, so nothing of the app shows before the prompt.
|
|
const [capsSeen, setCapsSeen] = useState<typeof caps | undefined>(undefined);
|
|
if (caps !== capsSeen) {
|
|
setCapsSeen(caps);
|
|
if (isAppLockEnabled()) setLocked(true);
|
|
}
|
|
useEffect(() => {
|
|
// Asked after the effect, not inside it: the prompt's answer sets state.
|
|
if (isAppLockEnabled()) void Promise.resolve().then(ask);
|
|
}, [caps, ask]);
|
|
|
|
// Back from the background after a while.
|
|
useEffect(() => {
|
|
if (!isAppLockEnabled()) return;
|
|
const listener = App.addListener('appStateChange', ({ isActive }) => {
|
|
if (!isActive) {
|
|
backgroundSince.current = Date.now();
|
|
return;
|
|
}
|
|
const since = backgroundSince.current;
|
|
backgroundSince.current = null;
|
|
if (since != null && Date.now() - since > RELOCK_AFTER_MS) {
|
|
setLocked(true);
|
|
void ask();
|
|
}
|
|
});
|
|
return () => {
|
|
void listener.then((handle) => handle.remove());
|
|
};
|
|
}, [caps, ask]);
|
|
|
|
if (!locked) return null;
|
|
|
|
return (
|
|
<div
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label={t('native.lock.title')}
|
|
className="fixed inset-0 z-[calc(var(--z-toast)+1)] flex flex-col items-center justify-center gap-5 bg-surface p-8 text-center backdrop-blur-2xl"
|
|
>
|
|
<span className="flex h-16 w-16 items-center justify-center rounded-full bg-surface-card text-content shadow-lg">
|
|
<Lock size={26} strokeWidth={2} aria-hidden="true" />
|
|
</span>
|
|
<p className="text-title font-semibold text-content">{t('native.lock.title')}</p>
|
|
<button
|
|
type="button"
|
|
onClick={() => void ask()}
|
|
className="rounded-full bg-accent px-6 py-3 text-body font-semibold text-accent-text transition-transform active:scale-[.98]"
|
|
>
|
|
{t('native.lock.unlock')}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|