A 7-slide carousel shown once on first launch: - the three tabs (Scenes/Movies/Sites) - search, filters, saved searches, Performers/Tags/Favorites - long-press actions (hide/duplicate a scene, remove a wrong performer, link diagnostics) - player gestures (tap controls, double-tap ±15s, swipe to scrub, unmute) - favorites, Hidden content, PIN lock, the ? report button, Sites ★ ratings Gated by a SecureStore flag; replayable from Settings ⚙ → Replay tutorial (via a tiny onboarding bus). Suppresses the What's-new popup for brand-new users (the tour covers it) and marks the changelog seen on finish. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
109 lines
3.8 KiB
TypeScript
109 lines
3.8 KiB
TypeScript
import * as SecureStore from 'expo-secure-store';
|
|
|
|
const URL_KEY = 'goon.backend_url';
|
|
const API_KEY = 'goon.api_key';
|
|
const DEVICE_KEY = 'goon.device_id';
|
|
|
|
// Stabilne ID instalacji — scope'uje stan usera (favorites/progress/blacklisty) na
|
|
// serwerze per urządzenie (bug 2026-06-08: stan był globalny, nowi userzy nadpisywali
|
|
// ulubione). Generowane raz, trzymane w SecureStore (przeżywa restart, NIE reinstalację).
|
|
let _deviceIdCache: string | null = null;
|
|
|
|
function _genDeviceId(): string {
|
|
// 32 hex chars — wystarczająco unikalne dla ID instalacji (nie security-critical).
|
|
let s = '';
|
|
for (let i = 0; i < 32; i++) s += Math.floor(Math.random() * 16).toString(16);
|
|
return s;
|
|
}
|
|
|
|
export async function getDeviceId(): Promise<string> {
|
|
if (_deviceIdCache) return _deviceIdCache;
|
|
let id = await SecureStore.getItemAsync(DEVICE_KEY);
|
|
if (!id) {
|
|
id = _genDeviceId();
|
|
await SecureStore.setItemAsync(DEVICE_KEY, id);
|
|
}
|
|
_deviceIdCache = id;
|
|
return id;
|
|
}
|
|
|
|
// Jednorazowe przejęcie legacy stanu (sprzed device-scopingu) — flaga per instalacja.
|
|
const LEGACY_ADOPTED_KEY = 'goon.legacy_adopted';
|
|
|
|
export async function isLegacyAdopted(): Promise<boolean> {
|
|
return (await SecureStore.getItemAsync(LEGACY_ADOPTED_KEY)) === '1';
|
|
}
|
|
|
|
export async function markLegacyAdopted(): Promise<void> {
|
|
await SecureStore.setItemAsync(LEGACY_ADOPTED_KEY, '1');
|
|
}
|
|
|
|
// "What's new" — ostatnio widziany wpis changelogu. Po dociągnięciu OTA bundle ma
|
|
// nowszy changelog → WhatsNewModal pokazuje różnicę i zapisuje najnowszy id.
|
|
const CHANGELOG_SEEN_KEY = 'goon.changelog_seen_id';
|
|
|
|
export async function getLastSeenChangelog(): Promise<string | null> {
|
|
return SecureStore.getItemAsync(CHANGELOG_SEEN_KEY);
|
|
}
|
|
|
|
export async function setLastSeenChangelog(id: string): Promise<void> {
|
|
await SecureStore.setItemAsync(CHANGELOG_SEEN_KEY, id);
|
|
}
|
|
|
|
// Onboarding/tutorial — pokazany raz przy pierwszym odpaleniu (carousel: tabs,
|
|
// funkcje, longpressy, gesty playera). Wersjonowany klucz → bump gdy tutorial
|
|
// istotnie się zmieni i chcemy pokazać ponownie. Replay z ustawień przez bus.
|
|
const ONBOARDING_SEEN_KEY = 'goon.onboarding_seen_v1';
|
|
|
|
export async function getOnboardingSeen(): Promise<boolean> {
|
|
try {
|
|
return (await SecureStore.getItemAsync(ONBOARDING_SEEN_KEY)) === '1';
|
|
} catch {
|
|
return true; // brak SecureStore → nie blokuj UI tutorialem w kółko
|
|
}
|
|
}
|
|
|
|
export async function setOnboardingSeen(seen: boolean): Promise<void> {
|
|
try {
|
|
if (seen) await SecureStore.setItemAsync(ONBOARDING_SEEN_KEY, '1');
|
|
else await SecureStore.deleteItemAsync(ONBOARDING_SEEN_KEY);
|
|
} catch {
|
|
// ignore — keychain quirks
|
|
}
|
|
}
|
|
|
|
// Liczba kolumn w siatkach scen (1/2/3). Feedback mobilism: domyślne 2 kol = za małe
|
|
// miniaturki na telefonie do rozpoznania performera. Default 2 (tablet-friendly).
|
|
const GRID_COLS_KEY = 'goon.grid_columns';
|
|
|
|
export async function getGridColumns(): Promise<number> {
|
|
const v = await SecureStore.getItemAsync(GRID_COLS_KEY);
|
|
const n = v ? parseInt(v, 10) : 2;
|
|
return n === 1 || n === 2 || n === 3 ? n : 2;
|
|
}
|
|
|
|
export async function setGridColumns(n: number): Promise<void> {
|
|
await SecureStore.setItemAsync(GRID_COLS_KEY, String(n));
|
|
}
|
|
|
|
export interface Credentials {
|
|
baseUrl: string;
|
|
apiKey: string;
|
|
}
|
|
|
|
export async function loadCredentials(): Promise<Credentials | null> {
|
|
const baseUrl = await SecureStore.getItemAsync(URL_KEY);
|
|
const apiKey = await SecureStore.getItemAsync(API_KEY);
|
|
if (!baseUrl || !apiKey) return null;
|
|
return { baseUrl, apiKey };
|
|
}
|
|
|
|
export async function saveCredentials(c: Credentials): Promise<void> {
|
|
await SecureStore.setItemAsync(URL_KEY, c.baseUrl);
|
|
await SecureStore.setItemAsync(API_KEY, c.apiKey);
|
|
}
|
|
|
|
export async function clearCredentials(): Promise<void> {
|
|
await SecureStore.deleteItemAsync(URL_KEY);
|
|
await SecureStore.deleteItemAsync(API_KEY);
|
|
}
|