Vorher: fill='#0a0a0a' (schwarz) auf bg-neutral-900 (schwarzem Button) → Logo unsichtbar. Erste TestFlight-Build (v0.3.0) hatte das noch drin — Fix für v0.3.0-rebuild oder v0.3.1 hotfix. Beide AppleIcon-Komponenten in signin.tsx + signup.tsx lokal dupliziert (nicht in shared component) → beide separat editiert. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
360 lines
12 KiB
TypeScript
360 lines
12 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
TextInput,
|
|
TouchableOpacity,
|
|
ActivityIndicator,
|
|
Platform,
|
|
} from 'react-native';
|
|
import { useRouter } from 'expo-router';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import Svg, { Path } from 'react-native-svg';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { useAuthStore, type DeviceLockedError } from '../../stores/auth';
|
|
import { KeyboardAwareScreen } from '../../components/KeyboardAwareScreen';
|
|
|
|
function GoogleIcon() {
|
|
return (
|
|
<Svg width={20} height={20} viewBox="0 0 24 24">
|
|
<Path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" />
|
|
<Path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
|
|
<Path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
|
|
<Path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
|
|
</Svg>
|
|
);
|
|
}
|
|
|
|
function AppleIcon() {
|
|
return (
|
|
<Svg width={20} height={20} viewBox="0 0 24 24" fill="#ffffff">
|
|
<Path d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" />
|
|
</Svg>
|
|
);
|
|
}
|
|
|
|
type OAuthProvider = 'google' | 'apple' | null;
|
|
|
|
function formatRemainingTime(isoTarget: string): string {
|
|
const ms = new Date(isoTarget).getTime() - Date.now();
|
|
if (ms <= 0) return '0min';
|
|
const totalMin = Math.floor(ms / 60_000);
|
|
const h = Math.floor(totalMin / 60);
|
|
const m = totalMin % 60;
|
|
if (h > 0) return `${h}h ${m}min`;
|
|
return `${m}min`;
|
|
}
|
|
|
|
function DeviceLockedPanel({
|
|
locked,
|
|
onBack,
|
|
}: {
|
|
locked: DeviceLockedError;
|
|
onBack: () => void;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const [remaining, setRemaining] = useState<string | null>(
|
|
locked.lockedUntil ? formatRemainingTime(locked.lockedUntil) : null
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!locked.lockedUntil) return;
|
|
const id = setInterval(() => {
|
|
setRemaining(formatRemainingTime(locked.lockedUntil!));
|
|
}, 30_000);
|
|
return () => clearInterval(id);
|
|
}, [locked.lockedUntil]);
|
|
|
|
return (
|
|
<View style={{ paddingHorizontal: 24, paddingVertical: 32, gap: 20 }}>
|
|
<View
|
|
style={{
|
|
width: 48,
|
|
height: 48,
|
|
borderRadius: 16,
|
|
backgroundColor: 'rgba(220,38,38,0.08)',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
}}
|
|
>
|
|
<Ionicons name="lock-closed" size={22} color="#dc2626" />
|
|
</View>
|
|
|
|
<Text
|
|
style={{
|
|
fontSize: 22,
|
|
fontFamily: 'Nunito_700Bold',
|
|
color: '#0a0a0a',
|
|
lineHeight: 28,
|
|
}}
|
|
>
|
|
{t('auth.device_locked_headline')}
|
|
</Text>
|
|
|
|
<Text
|
|
style={{
|
|
fontSize: 14,
|
|
fontFamily: 'Nunito_400Regular',
|
|
color: '#525252',
|
|
lineHeight: 22,
|
|
}}
|
|
>
|
|
{t('auth.device_locked_body')}
|
|
</Text>
|
|
|
|
{remaining ? (
|
|
<View
|
|
style={{
|
|
backgroundColor: 'rgba(245,158,11,0.1)',
|
|
borderRadius: 10,
|
|
paddingHorizontal: 14,
|
|
paddingVertical: 10,
|
|
}}
|
|
>
|
|
<Text
|
|
style={{
|
|
fontSize: 13,
|
|
fontFamily: 'Nunito_600SemiBold',
|
|
color: '#b45309',
|
|
}}
|
|
>
|
|
{t('auth.device_locked_countdown', { remaining })}
|
|
</Text>
|
|
</View>
|
|
) : null}
|
|
|
|
<View
|
|
style={{
|
|
backgroundColor: '#f5f5f5',
|
|
borderRadius: 10,
|
|
paddingHorizontal: 14,
|
|
paddingVertical: 12,
|
|
}}
|
|
>
|
|
<Text
|
|
style={{
|
|
fontSize: 12,
|
|
fontFamily: 'Nunito_400Regular',
|
|
color: '#737373',
|
|
lineHeight: 18,
|
|
}}
|
|
>
|
|
{t('auth.device_locked_email_hint')}
|
|
</Text>
|
|
</View>
|
|
|
|
{locked.releaseRequestable ? (
|
|
<TouchableOpacity
|
|
onPress={onBack}
|
|
activeOpacity={0.8}
|
|
style={{
|
|
backgroundColor: '#0a0a0a',
|
|
borderRadius: 14,
|
|
paddingVertical: 16,
|
|
alignItems: 'center',
|
|
}}
|
|
>
|
|
<Text
|
|
style={{
|
|
fontSize: 15,
|
|
fontFamily: 'Nunito_600SemiBold',
|
|
color: '#ffffff',
|
|
}}
|
|
>
|
|
{t('auth.device_locked_use_original')}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
) : null}
|
|
|
|
<TouchableOpacity onPress={onBack} activeOpacity={0.7} style={{ alignItems: 'center', paddingVertical: 8 }}>
|
|
<Text
|
|
style={{
|
|
fontSize: 13,
|
|
fontFamily: 'Nunito_400Regular',
|
|
color: '#a3a3a3',
|
|
}}
|
|
>
|
|
{t('auth.device_locked_back')}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const INPUT_STYLE = {
|
|
fontSize: 16,
|
|
lineHeight: 22,
|
|
paddingVertical: 14,
|
|
paddingHorizontal: 16,
|
|
color: '#0a0a0a',
|
|
fontFamily: 'Nunito_400Regular',
|
|
} as const;
|
|
|
|
export default function SignInScreen() {
|
|
const router = useRouter();
|
|
const { t } = useTranslation();
|
|
const { signInWithPassword, signInWithOAuth } = useAuthStore();
|
|
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [oauthLoading, setOauthLoading] = useState<OAuthProvider>(null);
|
|
const [deviceLocked, setDeviceLocked] = useState<DeviceLockedError | null>(null);
|
|
|
|
const onSubmit = async () => {
|
|
if (!email.trim() || !password) return;
|
|
setError(null);
|
|
setSubmitting(true);
|
|
const res = await signInWithPassword(email.trim(), password);
|
|
setSubmitting(false);
|
|
if (res.deviceLocked) {
|
|
setDeviceLocked(res.deviceLocked);
|
|
return;
|
|
}
|
|
if (res.error) {
|
|
setError(res.error);
|
|
return;
|
|
}
|
|
router.replace('/(app)');
|
|
};
|
|
|
|
const onOAuth = async (provider: 'google' | 'apple') => {
|
|
setError(null);
|
|
setOauthLoading(provider);
|
|
const res = await signInWithOAuth(provider);
|
|
setOauthLoading(null);
|
|
if (res.error) {
|
|
setError(res.error);
|
|
return;
|
|
}
|
|
router.replace('/(app)');
|
|
};
|
|
|
|
const isLoading = submitting || oauthLoading !== null;
|
|
|
|
if (deviceLocked) {
|
|
return (
|
|
<SafeAreaView style={{ flex: 1, backgroundColor: '#ffffff' }}>
|
|
<DeviceLockedPanel locked={deviceLocked} onBack={() => setDeviceLocked(null)} />
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<SafeAreaView className="flex-1 bg-white">
|
|
<KeyboardAwareScreen
|
|
scrollable
|
|
contentContainerStyle={{ flexGrow: 1, justifyContent: 'center', paddingHorizontal: 24 }}
|
|
>
|
|
<Text className="text-3xl text-neutral-900 mb-2" style={{ fontFamily: 'Nunito_700Bold' }}>{t('auth.welcomeBack')}</Text>
|
|
<Text className="text-base text-neutral-500 mb-8" style={{ fontFamily: 'Nunito_400Regular' }}>
|
|
{t('auth.signinSubtitle')}
|
|
</Text>
|
|
|
|
{/* OAuth Buttons */}
|
|
<TouchableOpacity
|
|
onPress={() => onOAuth('google')}
|
|
disabled={isLoading}
|
|
activeOpacity={0.8}
|
|
className="flex-row items-center justify-center gap-3 bg-white border border-neutral-200 rounded-xl mb-3 disabled:opacity-40"
|
|
style={{ paddingVertical: 14 }}
|
|
>
|
|
{oauthLoading === 'google' ? (
|
|
<ActivityIndicator color="#0a0a0a" size="small" />
|
|
) : (
|
|
<GoogleIcon />
|
|
)}
|
|
<Text className="text-neutral-900 text-base" style={{ fontFamily: 'Nunito_600SemiBold' }}>{t('auth.googleSignin')}</Text>
|
|
</TouchableOpacity>
|
|
|
|
{Platform.OS === 'ios' ? (
|
|
<TouchableOpacity
|
|
onPress={() => onOAuth('apple')}
|
|
disabled={isLoading}
|
|
activeOpacity={0.8}
|
|
className="flex-row items-center justify-center gap-3 bg-neutral-900 rounded-xl mb-6 disabled:opacity-40"
|
|
style={{ paddingVertical: 14 }}
|
|
>
|
|
{oauthLoading === 'apple' ? (
|
|
<ActivityIndicator color="white" size="small" />
|
|
) : (
|
|
<AppleIcon />
|
|
)}
|
|
<Text className="text-white text-base" style={{ fontFamily: 'Nunito_600SemiBold' }}>{t('auth.appleSignin')}</Text>
|
|
</TouchableOpacity>
|
|
) : null}
|
|
|
|
{/* Divider */}
|
|
<View className="flex-row items-center mb-6">
|
|
<View className="flex-1 h-px bg-neutral-200" />
|
|
<Text className="text-neutral-400 text-xs mx-3" style={{ fontFamily: 'Nunito_400Regular' }}>{t('auth.orWithEmail')}</Text>
|
|
<View className="flex-1 h-px bg-neutral-200" />
|
|
</View>
|
|
|
|
{/* Email + Password */}
|
|
<TextInput
|
|
className="bg-neutral-100 rounded-xl mb-3"
|
|
style={INPUT_STYLE}
|
|
placeholder={t('auth.emailPlaceholder')}
|
|
placeholderTextColor="#a3a3a3"
|
|
autoCapitalize="none"
|
|
autoComplete="email"
|
|
keyboardType="email-address"
|
|
value={email}
|
|
onChangeText={setEmail}
|
|
/>
|
|
|
|
<TextInput
|
|
className="bg-neutral-100 rounded-xl mb-1"
|
|
style={INPUT_STYLE}
|
|
placeholder={t('auth.passwordPlaceholder')}
|
|
placeholderTextColor="#a3a3a3"
|
|
secureTextEntry
|
|
autoComplete="password"
|
|
value={password}
|
|
onChangeText={setPassword}
|
|
/>
|
|
|
|
<TouchableOpacity
|
|
onPress={() => router.push('/forgot-password')}
|
|
activeOpacity={0.7}
|
|
className="self-end py-2 mb-4"
|
|
>
|
|
<Text className="text-rebreak-500 text-sm" style={{ fontFamily: 'Nunito_400Regular' }}>{t('auth.forgotPassword')}</Text>
|
|
</TouchableOpacity>
|
|
|
|
{error && (
|
|
<Text className="text-red-500 text-sm mb-3" style={{ fontFamily: 'Nunito_400Regular' }}>{error}</Text>
|
|
)}
|
|
|
|
<TouchableOpacity
|
|
onPress={onSubmit}
|
|
disabled={isLoading || !email || !password}
|
|
activeOpacity={0.8}
|
|
className="bg-rebreak-500 rounded-xl items-center mt-1 disabled:opacity-40"
|
|
style={{ paddingVertical: 16 }}
|
|
>
|
|
{submitting ? (
|
|
<ActivityIndicator color="white" size="small" />
|
|
) : (
|
|
<Text className="text-white text-base" style={{ fontFamily: 'Nunito_600SemiBold' }}>{t('auth.signin')}</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
onPress={() => router.push('/signup')}
|
|
activeOpacity={0.7}
|
|
className="py-4 items-center mt-2"
|
|
>
|
|
<Text className="text-neutral-500 text-sm" style={{ fontFamily: 'Nunito_400Regular' }}>
|
|
{t('auth.noAccount')}{' '}
|
|
<Text className="text-rebreak-500" style={{ fontFamily: 'Nunito_600SemiBold' }}>{t('auth.signup')}</Text>
|
|
</Text>
|
|
</TouchableOpacity>
|
|
</KeyboardAwareScreen>
|
|
</SafeAreaView>
|
|
);
|
|
}
|