Connect Apple Music and Spotify-via-Last.fm on iPhone

- Local Expo module (Swift): MusicKit authorization, Music User Token via
  MusicUserTokenProvider, and live Music app now-playing while the app is
  open (MediaPlayer notifications), reported through the device endpoint
- One-tap Apple Music onboarding on iOS; Apple Music row in Settings
- Sign in with Last.fm, plus a guided Spotify-via-Last.fm flow when direct
  Spotify sign-in is capped
- Device registration moved to shared code used by Android and iOS
Not yet compiled for iOS (no Xcode here); needs an EAS build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
2026-10-04 07:12:19 -04:00
co-authored by Claude Opus 5.5
parent ca97f1c3ac
commit 1d4aeee0dd
16 changed files with 522 additions and 78 deletions
+11 -1
View File
@@ -3,9 +3,11 @@ 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 { SWRConfig } from 'swr';
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 { SessionProvider, useSession } from '@/lib/session';
import { colors, space } from '@/lib/theme';
@@ -65,6 +67,7 @@ function AppNavigator() {
return () => sub.remove();
},
}}>
{signedIn && appleMusicAvailable && <AppleMusicLive />}
<Stack
screenOptions={{
headerStyle: { backgroundColor: colors.bg },
@@ -86,6 +89,13 @@ function AppNavigator() {
);
}
/** iOS: mirror the Music app live while the app is open, once Apple Music is connected. */
function AppleMusicLive() {
const { data: connections } = useSWR<ConnectionDTO[]>('/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 },
});