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>
92 lines
3.1 KiB
TypeScript
92 lines
3.1 KiB
TypeScript
import { AuthSession, type AuthState } from '@pipu/core';
|
|
import Constants from 'expo-constants';
|
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState, useSyncExternalStore, type ReactNode } from 'react';
|
|
|
|
import { currentServer, refreshStoreFor, rememberServer, setCurrentServer } from './storage';
|
|
|
|
interface SessionCtx {
|
|
/** Адрес сервера; null — сервер не выбран (экран подключения). */
|
|
server: string | null;
|
|
session: AuthSession | null;
|
|
ready: boolean;
|
|
connect: (url: string, version: string) => Promise<void>;
|
|
disconnect: () => Promise<void>;
|
|
}
|
|
|
|
const Ctx = createContext<SessionCtx | null>(null);
|
|
|
|
function deviceName(): string {
|
|
return Constants.deviceName ?? 'Android';
|
|
}
|
|
|
|
function makeSession(url: string) {
|
|
return new AuthSession({ baseUrl: url, clientType: 'android', deviceName: deviceName(), refreshStore: refreshStoreFor(url) });
|
|
}
|
|
|
|
/** Текущий сервер и сессия на нём; при запуске восстанавливает вход по сохранённому refresh-токену. */
|
|
export function SessionProvider({ children }: { children: ReactNode }) {
|
|
const [server, setServer] = useState<string | null>(null);
|
|
const [session, setSession] = useState<AuthSession | null>(null);
|
|
const [ready, setReady] = useState(false);
|
|
|
|
useEffect(() => {
|
|
void (async () => {
|
|
const url = await currentServer();
|
|
if (url) {
|
|
const s = makeSession(url);
|
|
await s.restore();
|
|
setServer(url);
|
|
setSession(s);
|
|
}
|
|
setReady(true);
|
|
})();
|
|
}, []);
|
|
|
|
const connect = useCallback(async (url: string, version: string) => {
|
|
await rememberServer({ url, version });
|
|
await setCurrentServer(url);
|
|
const s = makeSession(url);
|
|
await s.restore(); // на этом сервере уже могли войти раньше
|
|
setServer(url);
|
|
setSession(s);
|
|
}, []);
|
|
|
|
const disconnect = useCallback(async () => {
|
|
await setCurrentServer(null);
|
|
setServer(null);
|
|
setSession(null);
|
|
}, []);
|
|
|
|
const value = useMemo(() => ({ server, session, ready, connect, disconnect }), [server, session, ready, connect, disconnect]);
|
|
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
|
|
}
|
|
|
|
export function useSessionCtx(): SessionCtx {
|
|
const c = useContext(Ctx);
|
|
if (!c) throw new Error('useSessionCtx outside SessionProvider');
|
|
return c;
|
|
}
|
|
|
|
/** Сессия вошедшего пользователя (экраны внутри приложения). */
|
|
export function useSession(): AuthSession {
|
|
const { session } = useSessionCtx();
|
|
if (!session) throw new Error('no session');
|
|
return session;
|
|
}
|
|
|
|
const UNKNOWN: AuthState = { status: 'unknown' };
|
|
|
|
export function useAuthState(): AuthState {
|
|
const { session } = useSessionCtx();
|
|
return useSyncExternalStore(
|
|
(fn) => session?.subscribe(fn) ?? (() => undefined),
|
|
() => session?.state ?? UNKNOWN,
|
|
);
|
|
}
|
|
|
|
export function useUser() {
|
|
const state = useAuthState();
|
|
if (state.status !== 'signed_in') throw new Error('not signed in');
|
|
return state.user;
|
|
}
|