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:
@@ -1,24 +1,83 @@
|
||||
import { router } from 'expo-router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { Platform, StyleSheet, View } from 'react-native';
|
||||
import { mutate } from 'swr';
|
||||
import { Screen } from '@/components/Screen';
|
||||
import { Body, Button, Card, Title } from '@/components/ui';
|
||||
import { showError } from '@/lib/errors';
|
||||
import { markSetupSeen, openReaderSettings, syncDevice, useReader } from '@/lib/nowPlayingReader';
|
||||
import { AppleMusicPermissionError, connectAppleMusic } from '@/lib/appleMusic';
|
||||
import { openReaderSettings, syncDevice, useReader } from '@/lib/nowPlayingReader';
|
||||
import { markSetupSeen } from '@/lib/setup';
|
||||
import { colors, space } from '@/lib/theme';
|
||||
|
||||
/**
|
||||
* One-time onboarding on Android. This doubles as the prominent disclosure Google
|
||||
* Play requires before sending users to the notification-access screen.
|
||||
*/
|
||||
export default function ShareSetupScreen() {
|
||||
const { granted, playback } = useReader();
|
||||
const [synced, setSynced] = useState(false);
|
||||
const finishing = granted && !synced;
|
||||
const close = () => (router.canGoBack() ? router.back() : router.replace('/'));
|
||||
|
||||
export default function ShareSetupScreen() {
|
||||
useEffect(() => {
|
||||
markSetupSeen();
|
||||
}, []);
|
||||
return Platform.OS === 'ios' ? <AppleMusicSetup /> : <AndroidSetup />;
|
||||
}
|
||||
|
||||
/** iPhone: Apple Music is one system prompt. Other services live in Settings. */
|
||||
function AppleMusicSetup() {
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const connect = async () => {
|
||||
setConnecting(true);
|
||||
setError(null);
|
||||
try {
|
||||
await connectAppleMusic();
|
||||
await mutate('/connections');
|
||||
setConnected(true);
|
||||
} catch (err) {
|
||||
setError(err instanceof AppleMusicPermissionError ? err.message : "Couldn't connect Apple Music. Try again.");
|
||||
}
|
||||
setConnecting(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Screen edges={['top', 'bottom']} contentContainerStyle={styles.content}>
|
||||
<View style={{ gap: space.md }}>
|
||||
<Title>{connected ? "You're all set" : 'Share what you’re playing'}</Title>
|
||||
<Body muted>
|
||||
{connected
|
||||
? 'Play something in Apple Music and your friends will see it.'
|
||||
: 'Connect Apple Music and your friends see what you play. It takes one tap.'}
|
||||
</Body>
|
||||
</View>
|
||||
{!connected && (
|
||||
<Card>
|
||||
<Body style={styles.point}>• We see the songs you play in Apple Music, and nothing else.</Body>
|
||||
<Body style={styles.point}>• Use Spotify or Plex? Connect them in Settings.</Body>
|
||||
<Body style={styles.point}>• Pause sharing or disconnect any time.</Body>
|
||||
</Card>
|
||||
)}
|
||||
{error && <Body style={{ color: colors.danger }}>{error}</Body>}
|
||||
<View style={{ gap: space.sm }}>
|
||||
{connected ? (
|
||||
<Button variant="primary" title="Done" onPress={close} />
|
||||
) : (
|
||||
<>
|
||||
<Button variant="primary" title="Connect Apple Music" loading={connecting} onPress={connect} />
|
||||
<Button title="Not now" onPress={close} />
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Android: one toggle covers every music app. This screen doubles as the
|
||||
* prominent disclosure Google Play requires before the notification-access screen.
|
||||
*/
|
||||
function AndroidSetup() {
|
||||
const { granted, playback } = useReader();
|
||||
const [synced, setSynced] = useState(false);
|
||||
const finishing = granted && !synced;
|
||||
|
||||
useEffect(() => {
|
||||
if (!granted) return;
|
||||
@@ -27,8 +86,6 @@ export default function ShareSetupScreen() {
|
||||
.finally(() => setSynced(true));
|
||||
}, [granted]);
|
||||
|
||||
const close = () => (router.canGoBack() ? router.back() : router.replace('/'));
|
||||
|
||||
return (
|
||||
<Screen edges={['top', 'bottom']} contentContainerStyle={styles.content}>
|
||||
<View style={{ gap: space.md }}>
|
||||
|
||||
Reference in New Issue
Block a user