ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m39s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m2s
ci / ts / ts (packages + web) (push) Successful in 1m5s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 39s
- expo-router app in src/app: connect (server check, recent servers), login, mandatory credential / password change, tabs Home / Search / Collection / Profile, album and artist pages, full-screen player - AuthSession per server with the refresh token in expo-secure-store (Android Keystore); language from the profile - player on react-native-track-player: ExoPlayer queue with a notification and lock-screen controls via a playback service, 500 MB stream cache, quality by network (Wi-Fi high, cellular medium), play events with played time, reactions, playback state restore and the endless My Wave with feedback - home: pulsing wave circle driven by BPM / energy in the cover palette, activity and mood chips that switch the wave on the fly - New Architecture is off until react-native-track-player supports it; the release build runs codegen only when it is on; query-string is a direct dependency because expo-router 5.1 imports it undeclared Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
73 lines
3.0 KiB
TypeScript
73 lines
3.0 KiB
TypeScript
import { ApiError, unwrap } from '@pipu/core';
|
|
import { useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { ScrollView } from 'react-native';
|
|
|
|
import { useSession, useUser } from '../lib/session';
|
|
import { Button, ErrorBox, Field, H1, Muted, Screen, useErrorText } from '../ui/kit';
|
|
|
|
/** Обязательный шаг: новые логин и пароль админа после первого запуска или замена временного пароля. */
|
|
export default function SetupScreen() {
|
|
const { t } = useTranslation(['auth', 'common']);
|
|
const session = useSession();
|
|
const user = useUser();
|
|
const errorText = useErrorText();
|
|
const admin = user.must_change_credentials;
|
|
const min = user.role === 'user' ? 8 : 12;
|
|
const [username, setUsername] = useState(user.username);
|
|
const [password, setPassword] = useState('');
|
|
const [repeat, setRepeat] = useState('');
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState<unknown>(null);
|
|
const mismatch = repeat !== '' && repeat !== password;
|
|
const fieldErr = (f: string) => (error instanceof ApiError && error.fieldCode(f) ? errorText(error) : undefined);
|
|
|
|
const submit = async () => {
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
const res = admin
|
|
? await session.client.POST('/api/v1/me/bootstrap-credentials', { body: { new_username: username.trim(), new_password: password } })
|
|
: await session.client.POST('/api/v1/me/password', { body: { new_password: password } });
|
|
await session.apply(unwrap(res));
|
|
} catch (e) {
|
|
setError(e);
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Screen>
|
|
<ScrollView contentContainerStyle={{ padding: 20, gap: 14, flexGrow: 1, justifyContent: 'center' }} keyboardShouldPersistTaps="handled">
|
|
<H1>{t(admin ? 'setup.admin_title' : 'setup.password_title')}</H1>
|
|
<Muted>{t(admin ? 'setup.admin_text' : 'setup.password_text')}</Muted>
|
|
{admin && <Field label={t('setup.new_username')} value={username} onChangeText={setUsername} error={fieldErr('new_username')} />}
|
|
<Field
|
|
label={t('setup.new_password')}
|
|
value={password}
|
|
onChangeText={setPassword}
|
|
secure
|
|
hint={t('setup.min_length', { min })}
|
|
error={fieldErr('new_password')}
|
|
/>
|
|
<Field
|
|
label={t('setup.repeat_password')}
|
|
value={repeat}
|
|
onChangeText={setRepeat}
|
|
secure
|
|
error={mismatch ? t('common:form.passwords_mismatch') : undefined}
|
|
/>
|
|
<ErrorBox>{error && !fieldErr('new_password') && !fieldErr('new_username') ? errorText(error) : null}</ErrorBox>
|
|
<Button
|
|
title={t('setup.submit')}
|
|
onPress={() => void submit()}
|
|
busy={busy}
|
|
disabled={!password || password !== repeat || (admin && !username.trim())}
|
|
/>
|
|
<Button title={t('setup.logout')} variant="ghost" onPress={() => void session.logout()} />
|
|
</ScrollView>
|
|
</Screen>
|
|
);
|
|
}
|