CI / check (push) Canceled after 0s
"Forgot your password?" on web and mobile sends a one-time link (1 hour) to the account's email; the website's reset page sets the new password, signs out every other session and signs in. The request responds the same whether or not the email has an account, and the email goes out in the background so timing doesn't reveal it either. Only a hash of the link token is stored. Sends through SMTP_* settings; without them, emails are logged (and captured in tests). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
97 lines
3.5 KiB
TypeScript
97 lines
3.5 KiB
TypeScript
import { Image } from 'expo-image';
|
|
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 }}>
|
|
<Image source={require('@/assets/images/icon.png')} style={styles.logo} />
|
|
<Title style={{ fontSize: 36 }}>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="/forgot-password" style={styles.link}>
|
|
Forgot your password?
|
|
</Link>
|
|
)}
|
|
{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 },
|
|
logo: { width: 76, height: 76, borderRadius: 20, marginBottom: space.sm },
|
|
});
|