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:
@@ -0,0 +1,81 @@
|
||||
import { CameraView, useCameraPermissions, type BarcodeScanningResult } from 'expo-camera';
|
||||
import { router } from 'expo-router';
|
||||
import { useRef, useState } from 'react';
|
||||
import { Linking, StyleSheet, Text, View } from 'react-native';
|
||||
import { parseProfileLink } from '@musicshare/shared';
|
||||
import { Screen } from '@/components/Screen';
|
||||
import { Body, Button, Title } from '@/components/ui';
|
||||
import { useSession } from '@/lib/session';
|
||||
import { colors, radius, space } from '@/lib/theme';
|
||||
|
||||
/** Scan a friend's musicshare QR code and open their profile (with its Follow button). */
|
||||
export default function ScanScreen() {
|
||||
const { me } = useSession();
|
||||
const [permission, requestPermission] = useCameraPermissions();
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
// The camera reports the same code many times a second; act on the first.
|
||||
const handled = useRef(false);
|
||||
|
||||
const onScanned = ({ data }: BarcodeScanningResult) => {
|
||||
if (handled.current) return;
|
||||
const username = parseProfileLink(data);
|
||||
if (!username) {
|
||||
setMessage('That’s not a musicshare code.');
|
||||
return;
|
||||
}
|
||||
if (username === me?.username) {
|
||||
setMessage('That’s your own code. Show it to a friend instead!');
|
||||
return;
|
||||
}
|
||||
handled.current = true;
|
||||
router.replace(`/u/${username}`);
|
||||
};
|
||||
|
||||
if (!permission) return <Screen />;
|
||||
|
||||
if (!permission.granted) {
|
||||
return (
|
||||
<Screen edges={['bottom']} contentContainerStyle={styles.centered}>
|
||||
<Title>Scan a friend’s code</Title>
|
||||
<Body muted style={{ textAlign: 'center' }}>
|
||||
musicshare needs the camera to scan QR codes. It’s only used while this screen is open.
|
||||
</Body>
|
||||
{permission.canAskAgain ? (
|
||||
<Button variant="primary" title="Allow camera" onPress={requestPermission} />
|
||||
) : (
|
||||
<Button variant="primary" title="Open Settings" onPress={() => Linking.openSettings()} />
|
||||
)}
|
||||
<Button title="Show my code instead" onPress={() => router.replace('/qr')} />
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.fill}>
|
||||
<CameraView style={styles.fill} facing="back" barcodeScannerSettings={{ barcodeTypes: ['qr'] }} onBarcodeScanned={onScanned} />
|
||||
<View style={styles.overlay} pointerEvents="box-none">
|
||||
<View style={styles.frame} />
|
||||
<Text style={styles.prompt}>{message ?? 'Point at a friend’s musicshare code'}</Text>
|
||||
<Button title="Show my code" onPress={() => router.replace('/qr')} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
fill: { flex: 1, backgroundColor: '#000' },
|
||||
centered: { flexGrow: 1, justifyContent: 'center', gap: space.lg },
|
||||
overlay: { position: 'absolute', top: 0, right: 0, bottom: 0, left: 0, alignItems: 'center', justifyContent: 'center', gap: space.xl, padding: space.xl },
|
||||
frame: { width: 240, height: 240, borderRadius: radius.lg, borderWidth: 3, borderColor: colors.accentText },
|
||||
prompt: {
|
||||
color: colors.accentText,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.55)',
|
||||
paddingHorizontal: space.md,
|
||||
paddingVertical: space.sm,
|
||||
borderRadius: radius.md,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user