100 lines
3.3 KiB
TypeScript
100 lines
3.3 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Alert, KeyboardAvoidingView, Platform, Pressable, ScrollView, StyleSheet, Text } from 'react-native';
|
|
import { useRouter } from 'expo-router';
|
|
import { useMutation } from '@tanstack/react-query';
|
|
import { api } from '@/api';
|
|
import { useT } from '@/i18n';
|
|
import { AppBar } from '@/components/AppBar';
|
|
import { Button, Field, TextField } from '@/components/ui';
|
|
import { colors, fontSize, spacing } from '@/theme';
|
|
|
|
const MIN_PASSWORD = 8;
|
|
|
|
export default function ChangePasswordScreen() {
|
|
const router = useRouter();
|
|
const t = useT();
|
|
|
|
const [current, setCurrent] = useState('');
|
|
const [next, setNext] = useState('');
|
|
const [show, setShow] = useState(false);
|
|
|
|
const change = useMutation({
|
|
mutationFn: () => api.auth.changePassword(current, next),
|
|
onSuccess: () => {
|
|
// The backend keeps this device signed in and drops the others.
|
|
Alert.alert(t('changePassword.doneTitle'), t('changePassword.doneBody'), [
|
|
{ text: 'OK', onPress: () => router.back() },
|
|
]);
|
|
},
|
|
onError: (e: any) => {
|
|
const wrong = e?.fields?.current_password?.length;
|
|
Alert.alert(
|
|
t('changePassword.failed'),
|
|
wrong ? t('changePassword.wrongCurrent') : (e?.message ?? t('common.tryAgain')),
|
|
);
|
|
},
|
|
});
|
|
|
|
function onSubmit() {
|
|
if (!current) return Alert.alert(t('changePassword.failed'), t('changePassword.wrongCurrent'));
|
|
if (next.length < MIN_PASSWORD) {
|
|
return Alert.alert(
|
|
t('changePassword.failed'),
|
|
t('register.shortPassword', { count: MIN_PASSWORD }),
|
|
);
|
|
}
|
|
change.mutate();
|
|
}
|
|
|
|
return (
|
|
<KeyboardAvoidingView
|
|
style={{ flex: 1, backgroundColor: colors.bgApp }}
|
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
>
|
|
<AppBar title={t('changePassword.title')} back />
|
|
<ScrollView contentContainerStyle={styles.body} keyboardShouldPersistTaps="handled">
|
|
<Field label={t('changePassword.current')}>
|
|
<TextField
|
|
icon="lock-closed-outline"
|
|
value={current}
|
|
onChangeText={setCurrent}
|
|
secureTextEntry={!show}
|
|
autoCapitalize="none"
|
|
textContentType="password"
|
|
/>
|
|
</Field>
|
|
|
|
<Field label={t('changePassword.next')}>
|
|
<TextField
|
|
icon="key-outline"
|
|
value={next}
|
|
onChangeText={setNext}
|
|
placeholder={t('register.passwordHint', { count: MIN_PASSWORD })}
|
|
secureTextEntry={!show}
|
|
autoCapitalize="none"
|
|
textContentType="newPassword"
|
|
/>
|
|
<Pressable onPress={() => setShow((v) => !v)} style={styles.showPass} hitSlop={8}>
|
|
<Text style={styles.link}>{show ? t('login.hide') : t('login.show')}</Text>
|
|
</Pressable>
|
|
</Field>
|
|
|
|
<Button
|
|
title={t('changePassword.submit')}
|
|
icon="checkmark"
|
|
size="lg"
|
|
block
|
|
loading={change.isPending}
|
|
onPress={onSubmit}
|
|
/>
|
|
</ScrollView>
|
|
</KeyboardAvoidingView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
body: { padding: spacing[4], paddingBottom: spacing[9] },
|
|
showPass: { position: 'absolute', right: 14, top: 40 },
|
|
link: { color: colors.teal700, fontWeight: '600', fontSize: fontSize.sm },
|
|
});
|