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:
2026-10-05 19:16:40 -04:00
co-authored by Claude Opus 5.5
parent 17ae36064c
commit 3568b71e90
44 changed files with 1481 additions and 358 deletions
@@ -0,0 +1,72 @@
import { useState } from 'react';
import { Linking } from 'react-native';
import { mutate } from 'swr';
import { SERVICE_LABELS } from '@musicshare/shared';
import { SignInError, signInWithService } from '@/lib/connect';
import { Button } from '../ui';
import { ErrorText, SetupActions, SetupHeader, type SetupProps } from './common';
type Provider = 'plex' | 'lastfm';
/**
* Sign in to Plex, or to Last.fm for apps that can send plays there.
* (Spotify has its own SpotifySetup: via Last.fm or ListenBrainz.)
*/
export function ServiceSignInSetup({
service,
appName,
onDone,
skipLabel,
}: SetupProps & { service: Provider; appName?: string }) {
const [done, setDone] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const run = async () => {
setBusy(true);
setError(null);
try {
if ((await signInWithService(service)) === 'connected') {
await mutate('/connections');
setDone(true);
}
} catch (err) {
setError(err instanceof SignInError ? err.message : `Couldn't connect ${SERVICE_LABELS[service]}. Try again.`);
}
setBusy(false);
};
if (done) {
return (
<>
<SetupHeader
title="You're all set"
subtitle={`Play something on ${appName ?? SERVICE_LABELS[service]} and your friends will see it.`}
/>
<SetupActions primary="Continue" onPrimary={onDone} />
</>
);
}
if (service === 'lastfm') {
return (
<>
<SetupHeader
title="Connect Last.fm"
subtitle={`${appName ?? 'Your app'} doesn’t let other iPhone apps see what’s playing. If it can send plays to Last.fm (TIDAL can, in its settings), sign in to Last.fm and we’ll share them.`}
/>
<ErrorText>{error}</ErrorText>
<SetupActions primary="Sign in with Last.fm" loading={busy} onPrimary={run} onSkip={onDone} skipLabel={skipLabel} />
<Button small title="New to Last.fm? Create an account" onPress={() => Linking.openURL('https://www.last.fm/join')} />
</>
);
}
return (
<>
<SetupHeader title="Share what you’re playing" subtitle={`Sign in to ${SERVICE_LABELS[service]} and your friends see what you play.`} />
<ErrorText>{error}</ErrorText>
<SetupActions primary={`Connect ${SERVICE_LABELS[service]}`} loading={busy} onPrimary={run} onSkip={onDone} skipLabel={skipLabel} />
</>
);
}