Files
pipu-music/apps/mobile/src/app/setup.tsx
T
mrleo1nidandClaude Opus 5.5 9fd616da57
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
feat(mobile): sign-in, tabs, search, collection and background player (M5)
- 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>
2026-10-20 12:48:55 +03:00

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>
);
}