Add Expo mobile app for iOS and Android
Native tabs for feed, friend search, profile and settings, with feature parity with the web app. Sessions are stored in SecureStore, realtime disconnects in the background, and Spotify/Plex sign-in returns to the app via the URL scheme. 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,88 @@
|
||||
import { Link } from 'expo-router';
|
||||
import { useState } from 'react';
|
||||
import { KeyboardAvoidingView, Platform, StyleSheet, View } from 'react-native';
|
||||
import { api } from '@/lib/api';
|
||||
import { useSession } from '@/lib/session';
|
||||
import { colors, space } from '@/lib/theme';
|
||||
import { Screen } from './Screen';
|
||||
import { Body, Button, Field, Title } from './ui';
|
||||
|
||||
export function AuthForm({ mode }: { mode: 'login' | 'register' }) {
|
||||
const { signIn } = useSession();
|
||||
const [username, setUsername] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
const submit = async () => {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = mode === 'login' ? await api.login({ email, password }) : await api.register({ username, email, password });
|
||||
await signIn(res); // the protected stack swaps to the tabs automatically
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView style={{ flex: 1 }} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
|
||||
<Screen edges={mode === 'login' ? ['top'] : []} contentContainerStyle={styles.content}>
|
||||
{mode === 'login' && (
|
||||
<View style={{ gap: space.sm }}>
|
||||
<Title style={{ fontSize: 34 }}>musicshare</Title>
|
||||
<Body muted>See what your friends are playing, and open it in your own app.</Body>
|
||||
</View>
|
||||
)}
|
||||
{mode === 'register' && (
|
||||
<Field
|
||||
label="Username"
|
||||
value={username}
|
||||
onChangeText={setUsername}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
autoComplete="username-new"
|
||||
textContentType="username"
|
||||
/>
|
||||
)}
|
||||
<Field
|
||||
label="Email"
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
autoCapitalize="none"
|
||||
autoComplete="email"
|
||||
keyboardType="email-address"
|
||||
textContentType="emailAddress"
|
||||
/>
|
||||
<Field
|
||||
label="Password"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry
|
||||
autoComplete={mode === 'login' ? 'current-password' : 'new-password'}
|
||||
textContentType={mode === 'login' ? 'password' : 'newPassword'}
|
||||
onSubmitEditing={submit}
|
||||
returnKeyType="go"
|
||||
/>
|
||||
{error && <Body style={{ color: colors.danger }}>{error}</Body>}
|
||||
<Button variant="primary" title={mode === 'login' ? 'Log in' : 'Create account'} loading={busy} onPress={submit} />
|
||||
{mode === 'login' ? (
|
||||
<Link href="/register" style={styles.link}>
|
||||
New here? Create an account
|
||||
</Link>
|
||||
) : (
|
||||
<Link href="/login" style={styles.link} dismissTo>
|
||||
Already have an account? Log in
|
||||
</Link>
|
||||
)}
|
||||
</Screen>
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
content: { flexGrow: 1, justifyContent: 'center' },
|
||||
link: { color: colors.muted, textAlign: 'center', paddingVertical: space.sm },
|
||||
});
|
||||
Reference in New Issue
Block a user