fix(app): streamline startup loading states
This commit is contained in:
@@ -0,0 +1,232 @@
|
|||||||
|
import { expect, type Page } from "../fixtures";
|
||||||
|
import { buildCreateAgentPreferences, buildSeededHost } from "./daemon-registry";
|
||||||
|
|
||||||
|
const DISABLE_DEFAULT_SEED_ONCE_KEY = "@paseo:e2e-disable-default-seed-once";
|
||||||
|
const SEED_NONCE_KEY = "@paseo:e2e-seed-nonce";
|
||||||
|
const REGISTRY_KEY = "@paseo:daemon-registry";
|
||||||
|
const E2E_KEY = "@paseo:e2e";
|
||||||
|
|
||||||
|
type SavedHostInput = {
|
||||||
|
serverId: string;
|
||||||
|
label: string;
|
||||||
|
endpoint: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function startupScenario(page: Page) {
|
||||||
|
return new StartupScenario(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
class StartupScenario {
|
||||||
|
private readonly page: Page;
|
||||||
|
private savedHosts: SavedHostInput[] = [];
|
||||||
|
private desktopBridge = false;
|
||||||
|
private blockedEndpointPorts = new Set<string>();
|
||||||
|
private viewport: { width: number; height: number } | null = null;
|
||||||
|
|
||||||
|
constructor(page: Page) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
withMobileViewport(): this {
|
||||||
|
this.viewport = { width: 390, height: 844 };
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
withSavedHost(input: SavedHostInput): this {
|
||||||
|
this.savedHosts.push(input);
|
||||||
|
const port = input.endpoint.match(/:(\d+)$/)?.[1];
|
||||||
|
if (port) {
|
||||||
|
this.blockedEndpointPorts.add(port);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
withPendingDesktopDaemon(): this {
|
||||||
|
this.desktopBridge = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
withBlockedPort(port: string): this {
|
||||||
|
this.blockedEndpointPorts.add(port);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async openRoot(): Promise<StartupAssertions> {
|
||||||
|
await this.prepare();
|
||||||
|
await this.page.goto("/");
|
||||||
|
return new StartupAssertions(this.page);
|
||||||
|
}
|
||||||
|
|
||||||
|
async openHostWorkspace(input: {
|
||||||
|
serverId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
}): Promise<StartupAssertions> {
|
||||||
|
await this.prepare();
|
||||||
|
await this.page.goto(
|
||||||
|
`/h/${encodeURIComponent(input.serverId)}/workspace/${encodeURIComponent(input.workspaceId)}`,
|
||||||
|
);
|
||||||
|
return new StartupAssertions(this.page);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async prepare(): Promise<void> {
|
||||||
|
if (this.viewport) {
|
||||||
|
await this.page.setViewportSize(this.viewport);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.desktopBridge) {
|
||||||
|
await installPendingDesktopBridge(this.page);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const port of this.blockedEndpointPorts) {
|
||||||
|
await this.page.routeWebSocket(new RegExp(`:${escapeRegex(port)}\\b`), async (ws) => {
|
||||||
|
await ws.close({ code: 1008, reason: "Blocked unreachable startup test host." });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.savedHosts.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let the shared fixture create its seed nonce, then opt out of that seed for
|
||||||
|
// the next navigation so this scenario owns the stored host registry.
|
||||||
|
await this.page.goto("/");
|
||||||
|
const nowIso = new Date().toISOString();
|
||||||
|
const registry = this.savedHosts.map((host) =>
|
||||||
|
buildStoredHost({
|
||||||
|
serverId: host.serverId,
|
||||||
|
endpoint: host.endpoint,
|
||||||
|
label: host.label,
|
||||||
|
nowIso,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const firstHost = registry[0];
|
||||||
|
if (!firstHost) {
|
||||||
|
throw new Error("Expected at least one startup test host.");
|
||||||
|
}
|
||||||
|
const createAgentPreferences = buildStoredCreateAgentPreferences(firstHost.serverId);
|
||||||
|
|
||||||
|
await this.page.evaluate(
|
||||||
|
({ keys, registry, createAgentPreferences }) => {
|
||||||
|
const nonce = localStorage.getItem(keys.seedNonce);
|
||||||
|
if (!nonce) {
|
||||||
|
throw new Error("Expected e2e seed nonce before overriding startup registry.");
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(keys.e2e, "1");
|
||||||
|
localStorage.setItem(keys.registry, JSON.stringify(registry));
|
||||||
|
localStorage.setItem(
|
||||||
|
"@paseo:create-agent-preferences",
|
||||||
|
JSON.stringify(createAgentPreferences),
|
||||||
|
);
|
||||||
|
localStorage.setItem(keys.disableDefaultSeedOnce, nonce);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: {
|
||||||
|
disableDefaultSeedOnce: DISABLE_DEFAULT_SEED_ONCE_KEY,
|
||||||
|
e2e: E2E_KEY,
|
||||||
|
registry: REGISTRY_KEY,
|
||||||
|
seedNonce: SEED_NONCE_KEY,
|
||||||
|
},
|
||||||
|
registry,
|
||||||
|
createAgentPreferences,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StartupAssertions {
|
||||||
|
private readonly page: Page;
|
||||||
|
|
||||||
|
constructor(page: Page) {
|
||||||
|
this.page = page;
|
||||||
|
}
|
||||||
|
|
||||||
|
async expectsReconnectWelcome(): Promise<this> {
|
||||||
|
await expect(this.page.getByTestId("welcome-screen")).toBeVisible({ timeout: 15_000 });
|
||||||
|
await expect(this.page.getByText("Connecting…", { exact: true })).toBeVisible();
|
||||||
|
await expect(this.page.getByTestId("welcome-open-settings")).toBeVisible();
|
||||||
|
await expect(this.page.getByTestId("welcome-direct-connection")).toBeVisible();
|
||||||
|
await expect(this.page.getByTestId("welcome-paste-pairing-link")).toBeVisible();
|
||||||
|
await expect(this.page.getByTestId("welcome-scan-qr")).toHaveCount(0);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async expectsNoSavedHostStatus(input: { label: string }): Promise<this> {
|
||||||
|
await expect(this.page.getByText(input.label, { exact: true })).toHaveCount(0);
|
||||||
|
await expect(this.page.getByText("Connection error", { exact: true })).toHaveCount(0);
|
||||||
|
await expect(this.page.getByText("Offline", { exact: true })).toHaveCount(0);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async expectsNoLocalServerStartupCopy(): Promise<this> {
|
||||||
|
await expect(this.page.getByText("Starting local server...", { exact: true })).toHaveCount(0);
|
||||||
|
await expect(this.page.getByText("Connecting to local server...", { exact: true })).toHaveCount(
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async expectsDesktopDaemonStartup(): Promise<this> {
|
||||||
|
await expect(this.page.getByText("Starting local server...", { exact: true })).toBeVisible({
|
||||||
|
timeout: 15_000,
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async expectsSidebarHidden(): Promise<this> {
|
||||||
|
await expect(this.page.locator('[data-testid="sidebar-settings"]:visible')).toHaveCount(0);
|
||||||
|
await expect(this.page.locator('[data-testid="sidebar-project-list"]:visible')).toHaveCount(0);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installPendingDesktopBridge(page: Page): Promise<void> {
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
(window as unknown as { paseoDesktop: unknown }).paseoDesktop = {
|
||||||
|
platform: "darwin",
|
||||||
|
invoke: async (command: string) => {
|
||||||
|
if (command === "start_desktop_daemon") {
|
||||||
|
await new Promise(() => {
|
||||||
|
// Keep the daemon in the startup phase until the test ends.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (command === "desktop_daemon_status") {
|
||||||
|
return {
|
||||||
|
serverId: "srv_desktop_pending",
|
||||||
|
status: "starting",
|
||||||
|
listen: null,
|
||||||
|
hostname: null,
|
||||||
|
pid: null,
|
||||||
|
home: "",
|
||||||
|
version: null,
|
||||||
|
desktopManaged: true,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (command === "desktop_daemon_logs") {
|
||||||
|
return { logPath: "", contents: "" };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
getPendingOpenProject: async () => null,
|
||||||
|
events: { on: async () => () => undefined },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildStoredHost(input: {
|
||||||
|
serverId: string;
|
||||||
|
endpoint: string;
|
||||||
|
label: string;
|
||||||
|
nowIso: string;
|
||||||
|
}) {
|
||||||
|
return buildSeededHost(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildStoredCreateAgentPreferences(serverId: string) {
|
||||||
|
return buildCreateAgentPreferences(serverId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeRegex(value: string): string {
|
||||||
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { test } from "./fixtures";
|
||||||
|
import { startupScenario } from "./helpers/startup-dsl";
|
||||||
|
|
||||||
|
function getE2EDaemonPort(): string {
|
||||||
|
const port = process.env.E2E_DAEMON_PORT;
|
||||||
|
if (!port) {
|
||||||
|
throw new Error("E2E_DAEMON_PORT is not set.");
|
||||||
|
}
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getE2EServerId(): string {
|
||||||
|
const serverId = process.env.E2E_SERVER_ID;
|
||||||
|
if (!serverId) {
|
||||||
|
throw new Error("E2E_SERVER_ID is not set.");
|
||||||
|
}
|
||||||
|
return serverId;
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe("Startup loading presentation", () => {
|
||||||
|
test("mobile reconnect keeps connection recovery actions visible", async ({ page }) => {
|
||||||
|
const startup = await startupScenario(page)
|
||||||
|
.withMobileViewport()
|
||||||
|
.withSavedHost({
|
||||||
|
serverId: "srv_unreachable_mobile",
|
||||||
|
label: "Dev",
|
||||||
|
endpoint: "127.0.0.1:45678",
|
||||||
|
})
|
||||||
|
.openRoot();
|
||||||
|
|
||||||
|
await startup.expectsReconnectWelcome();
|
||||||
|
await startup.expectsNoSavedHostStatus({ label: "Dev" });
|
||||||
|
await startup.expectsNoLocalServerStartupCopy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("desktop daemon bootstrap keeps the local server startup copy desktop-only", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const startup = await startupScenario(page)
|
||||||
|
.withPendingDesktopDaemon()
|
||||||
|
.withBlockedPort(getE2EDaemonPort())
|
||||||
|
.openRoot();
|
||||||
|
|
||||||
|
await startup.expectsDesktopDaemonStartup();
|
||||||
|
await startup.expectsSidebarHidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("host-route refresh does not render route chrome around the bootstrap splash", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const serverId = getE2EServerId();
|
||||||
|
const startup = await startupScenario(page)
|
||||||
|
.withPendingDesktopDaemon()
|
||||||
|
.withBlockedPort(getE2EDaemonPort())
|
||||||
|
.openHostWorkspace({
|
||||||
|
serverId,
|
||||||
|
workspaceId: "workspace-1",
|
||||||
|
});
|
||||||
|
|
||||||
|
await startup.expectsDesktopDaemonStartup();
|
||||||
|
await startup.expectsSidebarHidden();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -817,8 +817,10 @@ function AppWithSidebar({ children }: { children: ReactNode }) {
|
|||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const params = useGlobalSearchParams<{ open?: string | string[] }>();
|
const params = useGlobalSearchParams<{ open?: string | string[] }>();
|
||||||
const hosts = useHosts();
|
const hosts = useHosts();
|
||||||
|
const storeReady = useStoreReady();
|
||||||
const activeServerId = useMemo(() => parseServerIdFromPathname(pathname), [pathname]);
|
const activeServerId = useMemo(() => parseServerIdFromPathname(pathname), [pathname]);
|
||||||
const shouldShowAppChrome = activeServerId !== null;
|
const shouldShowAppChrome =
|
||||||
|
storeReady && activeServerId !== null && hosts.some((host) => host.serverId === activeServerId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeServerId || hosts.length === 0) {
|
if (!activeServerId || hosts.length === 0) {
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useHostRuntimeBootstrapState, useStoreReady } from "@/app/_layout";
|
import { useHostRuntimeBootstrapState, useStoreReady } from "@/app/_layout";
|
||||||
|
import { shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon";
|
||||||
import { StartupSplashScreen } from "@/screens/startup-splash-screen";
|
import { StartupSplashScreen } from "@/screens/startup-splash-screen";
|
||||||
|
|
||||||
export function HostRouteBootstrapBoundary({ children }: { children: ReactNode }) {
|
export function HostRouteBootstrapBoundary({ children }: { children: ReactNode }) {
|
||||||
const storeReady = useStoreReady();
|
const storeReady = useStoreReady();
|
||||||
const bootstrapState = useHostRuntimeBootstrapState();
|
const bootstrapState = useHostRuntimeBootstrapState();
|
||||||
|
const isDesktop = shouldUseDesktopDaemon();
|
||||||
|
|
||||||
if (!storeReady) {
|
if (!storeReady) {
|
||||||
return <StartupSplashScreen bootstrapState={bootstrapState} />;
|
return <StartupSplashScreen bootstrapState={isDesktop ? bootstrapState : undefined} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
|
|||||||
@@ -5,12 +5,7 @@ import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
|||||||
import { QrCode, Link2, ClipboardPaste, ExternalLink, Settings } from "lucide-react-native";
|
import { QrCode, Link2, ClipboardPaste, ExternalLink, Settings } from "lucide-react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import type { HostProfile } from "@/types/host-connection";
|
import type { HostProfile } from "@/types/host-connection";
|
||||||
import {
|
import { getHostRuntimeStore, isHostRuntimeConnected, useHosts } from "@/runtime/host-runtime";
|
||||||
getHostRuntimeStore,
|
|
||||||
isHostRuntimeConnected,
|
|
||||||
useHostRuntimeSnapshot,
|
|
||||||
useHosts,
|
|
||||||
} from "@/runtime/host-runtime";
|
|
||||||
import { AddHostModal } from "./add-host-modal";
|
import { AddHostModal } from "./add-host-modal";
|
||||||
import { PairLinkModal } from "./pair-link-modal";
|
import { PairLinkModal } from "./pair-link-modal";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -94,39 +89,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
actionTextPrimary: {
|
actionTextPrimary: {
|
||||||
color: theme.colors.accentForeground,
|
color: theme.colors.accentForeground,
|
||||||
},
|
},
|
||||||
hostList: {
|
|
||||||
width: "100%",
|
|
||||||
maxWidth: 420,
|
|
||||||
marginTop: theme.spacing[6],
|
|
||||||
borderTopWidth: 1,
|
|
||||||
borderTopColor: theme.colors.border,
|
|
||||||
paddingTop: theme.spacing[4],
|
|
||||||
gap: theme.spacing[2],
|
|
||||||
},
|
|
||||||
hostRow: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing[3],
|
|
||||||
paddingVertical: theme.spacing[2],
|
|
||||||
},
|
|
||||||
statusDot: {
|
|
||||||
width: 8,
|
|
||||||
height: 8,
|
|
||||||
borderRadius: 4,
|
|
||||||
},
|
|
||||||
hostLabel: {
|
|
||||||
flex: 1,
|
|
||||||
color: theme.colors.foreground,
|
|
||||||
fontSize: theme.fontSize.sm,
|
|
||||||
},
|
|
||||||
hostStatus: {
|
|
||||||
color: theme.colors.foregroundMuted,
|
|
||||||
fontSize: theme.fontSize.sm,
|
|
||||||
},
|
|
||||||
hostStatusError: {
|
|
||||||
color: theme.colors.destructive,
|
|
||||||
fontSize: theme.fontSize.sm,
|
|
||||||
},
|
|
||||||
setupLink: {
|
setupLink: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -189,50 +151,6 @@ function useAnyHostOnline(serverIds: string[]): string | null {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function HostStatusRow({ serverId, label }: { serverId: string; label: string }) {
|
|
||||||
const { theme } = useUnistyles();
|
|
||||||
const snapshot = useHostRuntimeSnapshot(serverId);
|
|
||||||
const status = snapshot?.connectionStatus ?? "connecting";
|
|
||||||
const lastError = snapshot?.lastError ?? null;
|
|
||||||
|
|
||||||
let dotColor: string;
|
|
||||||
let statusText: string;
|
|
||||||
let isError = false;
|
|
||||||
|
|
||||||
switch (status) {
|
|
||||||
case "online":
|
|
||||||
dotColor = theme.colors.success;
|
|
||||||
statusText = "Online";
|
|
||||||
break;
|
|
||||||
case "connecting":
|
|
||||||
case "idle":
|
|
||||||
dotColor = theme.colors.foregroundMuted;
|
|
||||||
statusText = "Connecting…";
|
|
||||||
break;
|
|
||||||
case "offline":
|
|
||||||
dotColor = theme.colors.foregroundMuted;
|
|
||||||
statusText = "Offline";
|
|
||||||
break;
|
|
||||||
case "error":
|
|
||||||
dotColor = theme.colors.destructive;
|
|
||||||
statusText = lastError ? lastError.slice(0, 40) : "Connection error";
|
|
||||||
isError = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.hostRow}>
|
|
||||||
<View style={[styles.statusDot, { backgroundColor: dotColor }]} />
|
|
||||||
<Text style={styles.hostLabel} numberOfLines={1}>
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
<Text style={isError ? styles.hostStatusError : styles.hostStatus} numberOfLines={1}>
|
|
||||||
{statusText}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WelcomeScreenProps {
|
export interface WelcomeScreenProps {
|
||||||
onHostAdded?: (profile: HostProfile) => void;
|
onHostAdded?: (profile: HostProfile) => void;
|
||||||
}
|
}
|
||||||
@@ -306,7 +224,7 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const showHostList = hosts.length > 0 && !anyOnlineServerId;
|
const isConnectingToSavedHosts = hosts.length > 0 && !anyOnlineServerId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.root}>
|
<View style={styles.root}>
|
||||||
@@ -322,11 +240,11 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
|
|||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<PaseoLogo size={96} />
|
<PaseoLogo size={96} />
|
||||||
<View style={styles.copyBlock}>
|
<View style={styles.copyBlock}>
|
||||||
<Text style={styles.title}>Welcome to Paseo</Text>
|
{isConnectingToSavedHosts ? (
|
||||||
{showHostList ? (
|
<Text style={styles.subtitle}>Connecting…</Text>
|
||||||
<Text style={styles.subtitle}>Connecting to your hosts…</Text>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
<Text style={styles.title}>Welcome to Paseo</Text>
|
||||||
<Text style={styles.subtitle}>Connect your computer to get started</Text>
|
<Text style={styles.subtitle}>Connect your computer to get started</Text>
|
||||||
{isNative ? (
|
{isNative ? (
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -365,14 +283,6 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
|
|||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{showHostList && (
|
|
||||||
<View style={styles.hostList}>
|
|
||||||
{hosts.map((host) => (
|
|
||||||
<HostStatusRow key={host.serverId} serverId={host.serverId} label={host.label} />
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ export function StartupSplashScreen({ bootstrapState }: StartupSplashScreenProps
|
|||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<TitlebarDragRegion />
|
<TitlebarDragRegion />
|
||||||
<PaseoLogo size={96} />
|
<PaseoLogo size={96} />
|
||||||
<Text style={styles.subtitle}>Starting up…</Text>
|
<Text style={styles.subtitle}>Connecting…</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user