From 0def1a36191c0fa2739ed82cc60b33049932bb0a Mon Sep 17 00:00:00 2001 From: goon-foss Date: Mon, 12 Oct 2026 10:14:36 +0200 Subject: [PATCH] fix(mobile): PIN sklejany miedzy etapami, Forgot PIN, Cast na Huawei, wibracje w Edge PIN: kazdy etap (obecny, nowy, potwierdzenie) to ten sam PinEntry w tym samym miejscu, wiec React zostawial wpisane cyfry. Potwierdzenie startowalo z wpisanym juz PIN-em, a przy zmianie PIN-u nowe cyfry doklejaly sie do starych i zapisywal sie sklejony PIN (zgloszenie a8d85930 "I forgot my pin"). key={stage}. Emulator: na paczce z produkcji 1234 dwa razy = "PINs do not match", z poprawka zapisane. Forgot PIN? przy zmianie i wylaczaniu PIN-u: odcisk albo blokada ekranu telefonu (expo-local-authentication z device fallback); bez blokady komunikat. Sprawdzone na emulatorze w obu wariantach. Cast: Huawei z EMUI i uslugami Google (YAL-L21, Android 10) mial przy kazdym otwarciu filmu krach "background can not be translucent" w MediaRouteButton (GOON-82, 7 zdarzen). Przycisk Cast wylaczony na Huawei. Edge: bez wibracji przy przewinieciu (zgloszenie 12db400a). Co-Authored-By: Claude Opus 5.5 --- mobile/src/changelog.ts | 11 +++++ mobile/src/components/CastControl.tsx | 17 ++++++- mobile/src/components/swipe/ClipPager.tsx | 4 +- mobile/src/lib/applock.ts | 27 +++++++++++ mobile/src/screens/AppLockSettingsScreen.tsx | 48 +++++++++++++++++++- mobile/src/screens/PinEntry.tsx | 11 ++++- 6 files changed, 113 insertions(+), 5 deletions(-) diff --git a/mobile/src/changelog.ts b/mobile/src/changelog.ts index 1a91254..bc48d83 100644 --- a/mobile/src/changelog.ts +++ b/mobile/src/changelog.ts @@ -35,6 +35,17 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + id: '2026-10-12a', + date: 'October 2026', + items: [ + 'App lock: when you set or changed your PIN, the digits from the previous step stayed in, so a changed PIN could be saved as the old and new digits glued together. Fixed. If you got locked out of changing your PIN, use the new Forgot PIN? link and confirm with your fingerprint or phone screen lock.', + 'Edge no longer vibrates on every swipe.', + 'Huawei phones with Google services crashed every time a video opened. The Cast button is now off on Huawei, videos play.', + 'Searching performers also finds the name in reverse order and their aliases, so "Takase Rina" finds Rina Takase.', + 'Missing movie covers are back for most movies.', + ], + }, { id: '2026-10-06b', date: 'October 2026', diff --git a/mobile/src/components/CastControl.tsx b/mobile/src/components/CastControl.tsx index 7f87587..eb6da23 100644 --- a/mobile/src/components/CastControl.tsx +++ b/mobile/src/components/CastControl.tsx @@ -13,17 +13,32 @@ * w sobie niczego nie subskrybuje i jest bezpieczne wszędzie. */ import React from 'react'; -import { Alert } from 'react-native'; +import { Alert, Platform } from 'react-native'; import { CastButton, CastContext, useRemoteMediaClient } from 'react-native-google-cast'; import type { CastTarget } from '../lib/cast'; import { theme } from '../theme'; +/** + * Huawei z EMUI i uslugami Google (Honor 20, YAL-L21, Android 10): nakladka systemowa daje + * polprzezroczysty kolor motywu, a MediaRouteButton przy tworzeniu liczy na nim kontrast + * (androidx ColorUtils.calculateContrast) i rzuca "background can not be translucent". + * Aplikacja padala przy KAZDYM otwarciu filmu (GOON-82, 2026-10-08). Bez przycisku Cast + * film gra normalnie; na nowszych Huawei bez uslug Google Cast i tak nie dziala. + */ +function castCrashesOnThisDevice(): boolean { + if (Platform.OS !== 'android') return false; + const c = Platform.constants as { Manufacturer?: string; Brand?: string }; + const maker = `${c.Manufacturer ?? ''} ${c.Brand ?? ''}`.toLowerCase(); + return maker.includes('huawei'); +} + /** Czy na tym urządzeniu w ogóle da się castować. Null = jeszcze nie wiadomo. */ export function useCastAvailable(): boolean { const [available, setAvailable] = React.useState(false); React.useEffect(() => { let alive = true; + if (castCrashesOnThisDevice()) return; (async () => { try { const state = await CastContext.getPlayServicesState(); diff --git a/mobile/src/components/swipe/ClipPager.tsx b/mobile/src/components/swipe/ClipPager.tsx index 913a2b0..99e4867 100644 --- a/mobile/src/components/swipe/ClipPager.tsx +++ b/mobile/src/components/swipe/ClipPager.tsx @@ -10,7 +10,6 @@ * APK, tam zakladka Edge jest ukryta (`swipeAvailable`). */ import { createVideoPlayer, type VideoPlayer } from 'expo-video'; -import * as Haptics from 'expo-haptics'; import React from 'react'; import { AppState, StyleSheet, View } from 'react-native'; @@ -222,7 +221,8 @@ export function ClipPager({ // ignore } } - Haptics.selectionAsync().catch(() => {}); + // Bez wibracji przy przewinieciu (zgloszenie 12db400a: "bardzo meczace" przy + // przewijaniu jednego klipu za drugim). return () => { sub.remove(); subStatus.remove(); diff --git a/mobile/src/lib/applock.ts b/mobile/src/lib/applock.ts index 1b2e1ba..204ad7a 100644 --- a/mobile/src/lib/applock.ts +++ b/mobile/src/lib/applock.ts @@ -64,6 +64,33 @@ export async function biometricAvailable(): Promise { return enrolled; } +/** Czy telefon ma jakakolwiek blokade (odcisk, twarz albo PIN/wzor ekranu). */ +export async function ownerAuthAvailable(): Promise { + try { + const level = await LocalAuthentication.getEnrolledLevelAsync(); + return level !== LocalAuthentication.SecurityLevel.NONE; + } catch { + return false; + } +} + +/** + * Potwierdzenie, ze to wlasciciel telefonu: odcisk albo blokada ekranu Androida. + * Do resetu zapomnianego PIN-u (zgloszenie a8d85930: odblokowanie palcem dzialalo, ale + * zmiana albo wylaczenie PIN-u wymagaly starego, ktorego user nie pamietal). + */ +export async function authenticateOwner(): Promise { + try { + const res = await LocalAuthentication.authenticateAsync({ + promptMessage: 'Confirm it is you', + disableDeviceFallback: false, + }); + return res.success; + } catch { + return false; + } +} + export async function authenticateBiometric(): Promise { const res = await LocalAuthentication.authenticateAsync({ promptMessage: 'Unlock Goon', diff --git a/mobile/src/screens/AppLockSettingsScreen.tsx b/mobile/src/screens/AppLockSettingsScreen.tsx index 13f5ae8..e0f6e39 100644 --- a/mobile/src/screens/AppLockSettingsScreen.tsx +++ b/mobile/src/screens/AppLockSettingsScreen.tsx @@ -17,10 +17,12 @@ import { useClient } from '../ClientContext'; import { getDeviceId } from '../storage'; import { AppLockSettings, + authenticateOwner, biometricAvailable, clearPin, DEFAULT_TIMEOUT_SECONDS, getSettings, + ownerAuthAvailable, setBioEnabled, setEnabled, setPin, @@ -59,6 +61,7 @@ type Stage = 'menu' | 'enter-current' | 'set-new' | 'confirm-new' | 'disable-con export function AppLockSettingsScreen({ onLogout }: { onLogout?: () => void }) { const [settings, setSettings] = useState(null); const [bioAvailable, setBioAvailable] = useState(false); + const [ownerAuth, setOwnerAuth] = useState(false); const [stage, setStage] = useState('menu'); const [newPin, setNewPin] = useState(''); const [errorText, setErrorText] = useState(null); @@ -74,9 +77,15 @@ export function AppLockSettingsScreen({ onLogout }: { onLogout?: () => void }) { const [importing, setImporting] = useState(false); async function refresh() { - const [s, bio, dev] = await Promise.all([getSettings(), biometricAvailable(), getDeviceId()]); + const [s, bio, dev, owner] = await Promise.all([ + getSettings(), + biometricAvailable(), + getDeviceId(), + ownerAuthAvailable(), + ]); setSettings(s); setBioAvailable(bio); + setOwnerAuth(owner); setDeviceId(dev); } @@ -118,6 +127,27 @@ export function AppLockSettingsScreen({ onLogout }: { onLogout?: () => void }) { } }; + // Zapomniany PIN: odcisk albo blokada ekranu telefonu zamiast starego PIN-u. Bez blokady + // na telefonie nie ma czym potwierdzic wlasciciela, wiec tylko wyjasniamy, co zrobic. + const forgotPin = (onConfirmed: () => Promise | void) => ({ + label: 'Forgot PIN?', + onPress: async () => { + if (!ownerAuth) { + Alert.alert( + 'Forgot PIN', + 'This phone has no fingerprint or screen lock, so Goon cannot check it is you. ' + + 'Reinstalling the app removes the PIN. Copy your sync code first (Backup & sync below) ' + + 'to keep your favourites.', + ); + return; + } + if (await authenticateOwner()) { + setErrorText(null); + await onConfirmed(); + } + }, + }); + if (!settings) { return ( @@ -126,9 +156,14 @@ export function AppLockSettingsScreen({ onLogout }: { onLogout?: () => void }) { ); } + // key={stage} przy kazdym PinEntry: bez niego React zostawial ten sam komponent miedzy + // etapami i wpisane cyfry przechodzily dalej. Potwierdzenie startowalo z wpisanym juz + // PIN-em, a przy zmianie PIN-u nowe cyfry doklejaly sie do starych i zapisywal sie + // sklejony PIN (np. 11112222), ktorego user nie znal (zgloszenie a8d85930). if (stage === 'enter-current') { return ( { @@ -144,6 +179,7 @@ export function AppLockSettingsScreen({ onLogout }: { onLogout?: () => void }) { setErrorText(null); setStage('set-new'); }} + footerAction={forgotPin(() => setStage('set-new'))} /> ); } @@ -151,6 +187,7 @@ export function AppLockSettingsScreen({ onLogout }: { onLogout?: () => void }) { if (stage === 'set-new') { return ( { @@ -174,6 +211,7 @@ export function AppLockSettingsScreen({ onLogout }: { onLogout?: () => void }) { if (stage === 'confirm-new') { return ( { @@ -201,6 +239,7 @@ export function AppLockSettingsScreen({ onLogout }: { onLogout?: () => void }) { if (stage === 'disable-confirm') { return ( { @@ -220,6 +259,13 @@ export function AppLockSettingsScreen({ onLogout }: { onLogout?: () => void }) { setStage('menu'); await refresh(); }} + footerAction={forgotPin(async () => { + await setEnabled(false); + await setBioEnabled(false); + await clearPin(); + setStage('menu'); + await refresh(); + })} /> ); } diff --git a/mobile/src/screens/PinEntry.tsx b/mobile/src/screens/PinEntry.tsx index 2322e6b..32b4c1a 100644 --- a/mobile/src/screens/PinEntry.tsx +++ b/mobile/src/screens/PinEntry.tsx @@ -8,11 +8,13 @@ interface Props { error?: string | null; onSubmit: (pin: string) => Promise | void; onCancel: () => void; + /** Maly link pod przyciskami, np. "Forgot PIN?". */ + footerAction?: { label: string; onPress: () => void }; } const KEYS: (string | null)[] = ['1', '2', '3', '4', '5', '6', '7', '8', '9', null, '0', '⌫']; -export function PinEntry({ title, error, onSubmit, onCancel }: Props) { +export function PinEntry({ title, error, onSubmit, onCancel, footerAction }: Props) { const [pin, setPin] = useState(''); function press(k: string) { @@ -67,6 +69,11 @@ export function PinEntry({ title, error, onSubmit, onCancel }: Props) { Next + {footerAction ? ( + + {footerAction.label} + + ) : null} ); } @@ -75,6 +82,8 @@ const KEY_SIZE = 72; const styles = StyleSheet.create({ root: { flex: 1, backgroundColor: theme.bg, alignItems: 'center', paddingTop: 60 }, + footerAction: { marginTop: 22, padding: 6 }, + footerActionText: { color: theme.muted, fontSize: 14, textDecorationLine: 'underline' }, title: { color: theme.fg, fontSize: 18, fontWeight: '700' }, error: { color: theme.bad, fontSize: 13, marginTop: 8 }, dots: { flexDirection: 'row', gap: 14, marginTop: 14, minHeight: 16 },