119 lines · 4.0 KB · TypeScript
RawDownload
import { requireOptionalNativeModule } from "expo"; import { MOBILE_THEME_IDS, type MobileThemeId } from "../../lib/mobileTheme"; export const SHOWCASE_SCENES = ["threads", "thread", "terminal", "review", "environments"] as const;export type ShowcaseScene = (typeof SHOWCASE_SCENES)[number]; export type ShowcaseOrientation = "portrait" | "landscape"; interface NativeShowcaseControls {  readonly getShowcasePairingUrl?: () => string | null;  readonly getShowcaseScene?: () => string | null;  readonly getShowcaseTheme?: () => string | null;  readonly getShowcaseOrientation?: () => string | null;  readonly applyShowcaseOrientation?: (orientation: ShowcaseOrientation) => Promise<void>;  readonly getInterfaceOrientation?: () => Promise<string>;  readonly prepareShowcaseCapture?: () => void;  readonly markShowcaseReady?: (scene: ShowcaseScene) => void;} function nativeShowcaseControls(): NativeShowcaseControls | null {  return requireOptionalNativeModule<NativeShowcaseControls>("T3NativeControls");} export function getNativeShowcasePairingUrls(): ReadonlyArray<string> {  try {    let raw = nativeShowcaseControls()?.getShowcasePairingUrl?.()?.trim();    if (!raw) return [];    if (raw.startsWith("json-uri:")) {      try {        raw = decodeURIComponent(raw.slice("json-uri:".length));      } catch {        return [];      }    }    try {      const parsed: unknown = JSON.parse(raw);      if (Array.isArray(parsed)) {        return parsed.filter(          (candidate): candidate is string =>            typeof candidate === "string" && candidate.trim().length > 0,        );      }    } catch {      // Older runners pass a single URL rather than a JSON array.    }    return [raw];  } catch {    return [];  }} export function getNativeShowcaseScene(): ShowcaseScene | null {  try {    const scene = nativeShowcaseControls()?.getShowcaseScene?.()?.trim();    return SHOWCASE_SCENES.find((candidate) => candidate === scene) ?? null;  } catch {    return null;  }} /** * Returns null when the runner requested no palette, which leaves the stored * theme preference untouched. An unknown id also reads as null rather than * silently falling back, so a capture never claims to show a theme it does not. */export function getNativeShowcaseTheme(): MobileThemeId | null {  try {    const theme = nativeShowcaseControls()?.getShowcaseTheme?.()?.trim();    return MOBILE_THEME_IDS.find((candidate) => candidate === theme) ?? null;  } catch {    return null;  }} export function prepareNativeShowcaseCapture(): void {  try {    nativeShowcaseControls()?.prepareShowcaseCapture?.();  } catch {    // The harness still works when a development build predates this helper.  }} export function getNativeShowcaseOrientation(): ShowcaseOrientation | null {  try {    const orientation = nativeShowcaseControls()?.getShowcaseOrientation?.()?.trim();    return orientation === "portrait" || orientation === "landscape" ? orientation : null;  } catch {    return null;  }} export async function applyNativeShowcaseOrientation(  orientation: ShowcaseOrientation,): Promise<boolean> {  const controls = nativeShowcaseControls();  if (!controls?.applyShowcaseOrientation || !controls.getInterfaceOrientation) {    // A development build that predates this helper keeps its default    // orientation; report success so callers do not retry forever.    return true;  }  try {    await controls.applyShowcaseOrientation(orientation);    // The geometry request settles asynchronously; confirm it took effect so    // callers can retry attempts made before the scene was foreground-active.    await new Promise((resolve) => setTimeout(resolve, 500));    return (await controls.getInterfaceOrientation()) === orientation;  } catch {    return false;  }} export function markNativeShowcaseReady(scene: ShowcaseScene): void {  try {    nativeShowcaseControls()?.markShowcaseReady?.(scene);  } catch {    // The readiness marker is capture-runner metadata, never app functionality.  }}