Mobile: redesign, onboarding, QR codes, logo, Spotify setup
- Redesign matching the web: album-art backdrops, Reanimated equalizer, spinning live ring, drifting ambient background, gradient buttons with spring press feedback and haptics, Space Grotesk headings, animated feed (enter + layout transitions); fix invisible tab labels - Four-step onboarding for new accounts: which app you listen with (sets where songs open), the matching share setup, find friends (search, scan, invite), and a featured song. Skippable; stored server-side - QR codes: My code (share link) and an in-app scanner (camera only, no microphone permission) - Spotify setup via Last.fm or ListenBrainz with guided linking; Android shares Spotify automatically through the phone reader - New logo as app icon, adaptive/monochrome icon and splash - app.config.ts allows cleartext only for local http:// test builds Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
@@ -1,132 +1,26 @@
|
||||
import { router } from 'expo-router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Platform, StyleSheet, View } from 'react-native';
|
||||
import { mutate } from 'swr';
|
||||
import { useEffect } from 'react';
|
||||
import { Platform, StyleSheet } from 'react-native';
|
||||
import { Screen } from '@/components/Screen';
|
||||
import { Body, Button, Card, Title } from '@/components/ui';
|
||||
import { showError } from '@/lib/errors';
|
||||
import { AppleMusicPermissionError, connectAppleMusic } from '@/lib/appleMusic';
|
||||
import { openReaderSettings, syncDevice, useReader } from '@/lib/nowPlayingReader';
|
||||
import { AndroidShareSetup } from '@/components/setup/AndroidShareSetup';
|
||||
import { AppleMusicSetup } from '@/components/setup/AppleMusicSetup';
|
||||
import { markSetupSeen } from '@/lib/setup';
|
||||
import { colors, space } from '@/lib/theme';
|
||||
import { space } from '@/lib/theme';
|
||||
|
||||
const close = () => (router.canGoBack() ? router.back() : router.replace('/'));
|
||||
|
||||
/** Standalone "share what you're playing" (from the feed card), outside onboarding. */
|
||||
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;
|
||||
syncDevice()
|
||||
.catch((err) => showError(err, "Couldn't finish setup"))
|
||||
.finally(() => setSynced(true));
|
||||
}, [granted]);
|
||||
|
||||
return (
|
||||
<Screen edges={['top', 'bottom']} contentContainerStyle={styles.content}>
|
||||
<View style={{ gap: space.md }}>
|
||||
<Title>{granted ? "You're all set" : 'Share what you’re playing'}</Title>
|
||||
{granted ? (
|
||||
<Body muted>
|
||||
{playback?.title
|
||||
? `Friends can see you're listening to ${playback.title}${playback.artist ? ` by ${playback.artist}` : ''}.`
|
||||
: 'Play something in Spotify, YouTube Music, Apple Music or any music app and your friends will see it.'}
|
||||
</Body>
|
||||
) : (
|
||||
<Body muted>
|
||||
Works with Spotify, YouTube Music, Apple Music, TIDAL, Deezer, SoundCloud and more. No accounts to link.
|
||||
</Body>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{!granted && (
|
||||
<Card>
|
||||
<Body style={styles.point}>• We read the song, artist and album from music apps, and nothing else.</Body>
|
||||
<Body style={styles.point}>• We never see your notifications, messages or other apps.</Body>
|
||||
<Body style={styles.point}>• Android will call this “notification access”. That’s the only way it lets apps see what’s playing.</Body>
|
||||
<Body style={styles.point}>• Turn it off any time in Settings.</Body>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<View style={{ gap: space.sm }}>
|
||||
{granted ? (
|
||||
<Button variant="primary" title="Done" loading={finishing} onPress={close} />
|
||||
) : (
|
||||
<>
|
||||
<Button variant="primary" title="Turn on" onPress={openReaderSettings} />
|
||||
<Button title="Not now" onPress={close} />
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
{Platform.OS === 'ios' ? <AppleMusicSetup onDone={close} /> : <AndroidShareSetup onDone={close} />}
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
content: { flexGrow: 1, justifyContent: 'center', gap: space.xl },
|
||||
point: { color: colors.text, fontSize: 14 },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user