import { Poppins_400Regular, Poppins_500Medium, Poppins_600SemiBold, useFonts } from '@expo-google-fonts/poppins';
import { DarkTheme, Stack, ThemeProvider } from 'expo-router';
import * as SplashScreen from 'expo-splash-screen';
import { StatusBar } from 'expo-status-bar';
import { useEffect } from 'react';
import { AppState, StyleSheet, View } from 'react-native';
import useSWR, { SWRConfig } from 'swr';
import type { ConnectionDTO } from '@musicshare/shared';
import { Body, Button, Title } from '@/components/ui';
import { api } from '@/lib/api';
import { appleMusicAvailable, useAppleMusicLive } from '@/lib/appleMusic';
import { syncDevice } from '@/lib/nowPlayingReader';
import { syncPush } from '@/lib/push';
import { SessionProvider, useSession } from '@/lib/session';
import { startUpdater } from '@/lib/updates';
import { colors, space } from '@/lib/theme';
SplashScreen.preventAutoHideAsync();
const theme = {
...DarkTheme,
colors: { ...DarkTheme.colors, background: colors.bg, card: colors.bg, text: colors.text, primary: colors.accent, border: colors.border },
};
export default function RootLayout() {
return (
);
}
function AppNavigator() {
const { status, me, retry } = useSession();
const signedIn = status === 'signedIn';
const onboarded = Boolean(me?.onboardedAt);
const [fontsLoaded, fontError] = useFonts({ Poppins_400Regular, Poppins_500Medium, Poppins_600SemiBold });
const ready = status !== 'loading' && (fontsLoaded || Boolean(fontError));
useEffect(() => {
if (ready) SplashScreen.hideAsync();
}, [ready]);
// Look for a newer version of the app (our own APKs only; see lib/updates.ts).
useEffect(() => {
if (ready) startUpdater();
}, [ready]);
// Keep the Android listener pointed at this account (no-op elsewhere).
useEffect(() => {
if (signedIn) syncDevice().catch(() => {});
if (signedIn) syncPush().catch(() => {});
}, [signedIn]);
if (!ready) return null;
if (status === 'offline') {
return (
Can't reach musicshare
Check your connection and try again.
);
}
return (
// Keyed by user so signing out (or in as someone else) starts from an empty cache.
api.get(path),
provider: () => new Map(),
isVisible: () => AppState.currentState === 'active',
initFocus(callback) {
const sub = AppState.addEventListener('change', (s) => s === 'active' && callback());
return () => sub.remove();
},
}}>
{signedIn && appleMusicAvailable && }
{/* New accounts go through onboarding first (finishing flips onboardedAt and lands on the feed). */}
{/* Reachable from onboarding too (scanning a friend's code, viewing a profile). */}
);
}
/** iOS: mirror the Music app live while the app is open, once Apple Music is connected. */
function AppleMusicLive() {
const { data: connections } = useSWR('/connections');
useAppleMusicLive(Boolean(connections?.some((c) => c.provider === 'apple_music' && c.status === 'active')));
return null;
}
const styles = StyleSheet.create({
offline: { flex: 1, backgroundColor: colors.bg, justifyContent: 'center', padding: space.xl, gap: space.md },
});