82 lines
2.7 KiB
TypeScript
82 lines
2.7 KiB
TypeScript
import React, { Ref, useCallback, useEffect, useLayoutEffect, useRef } from 'react';
|
||
|
||
import { launchSplash } from '../../plugins/launchSplash';
|
||
import {
|
||
attachMascotVideo,
|
||
detachMascotVideo,
|
||
mascotVideoReady,
|
||
} from './mascotSingleton';
|
||
import * as css from './styles.css';
|
||
|
||
type AuthMascotProps = {
|
||
mascotRef?: Ref<HTMLDivElement>;
|
||
// True for the splash-gate variant (cold start, sync wait): mascot is
|
||
// centered both axes to mirror the Android 12+ system splash icon, so the
|
||
// native → WebView → web-splash handoff reads as one continuous splash.
|
||
// False (default) keeps the auth-page positioning (top-anchored) used by
|
||
// login / register / reset-password.
|
||
centered?: boolean;
|
||
};
|
||
|
||
export function AuthMascot({ mascotRef, centered }: AuthMascotProps) {
|
||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||
const didSignalLaunchReadyRef = useRef(false);
|
||
|
||
// Singleton <video> attach/detach. Preserves currentTime across the
|
||
// ConfigConfigLoading → SpecVersionsLoader → ClientRootLoading remounts
|
||
// so the mascot loop reads as one continuous animation instead of two
|
||
// restarts mid-boot.
|
||
useLayoutEffect(() => {
|
||
const container = containerRef.current;
|
||
if (!container) return undefined;
|
||
attachMascotVideo(container, css.AuthMascotVideo);
|
||
return () => {
|
||
detachMascotVideo(container);
|
||
};
|
||
}, []);
|
||
|
||
// Native splash hand-off: only the centered (splash-gate) variant signals
|
||
// the Android system splash to drop. Auth pages (login/register) don't
|
||
// signal — they're already past the cold-boot phase by the time they
|
||
// mount. requestAnimationFrame×2 ensures the browser has actually
|
||
// committed the paint before we ask the OS to release its splash.
|
||
const signalLaunchReady = useCallback(() => {
|
||
if (!centered || didSignalLaunchReadyRef.current) return;
|
||
didSignalLaunchReadyRef.current = true;
|
||
requestAnimationFrame(() => {
|
||
requestAnimationFrame(() => {
|
||
launchSplash.ready();
|
||
});
|
||
});
|
||
}, [centered]);
|
||
|
||
useEffect(() => {
|
||
if (!centered) return undefined;
|
||
let cancelled = false;
|
||
mascotVideoReady().then(() => {
|
||
if (!cancelled) signalLaunchReady();
|
||
});
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, [centered, signalLaunchReady]);
|
||
|
||
const setRefs = useCallback(
|
||
(node: HTMLDivElement | null) => {
|
||
containerRef.current = node;
|
||
if (typeof mascotRef === 'function') mascotRef(node);
|
||
else if (mascotRef && typeof mascotRef === 'object') {
|
||
(mascotRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
||
}
|
||
},
|
||
[mascotRef]
|
||
);
|
||
|
||
return (
|
||
<div
|
||
className={centered ? css.AuthMascotCentered : css.AuthMascot}
|
||
aria-hidden="true"
|
||
ref={setRefs}
|
||
/>
|
||
);
|
||
}
|