fix(mobile): PIN sklejany miedzy etapami, Forgot PIN, Cast na Huawei, wibracje w Edge
Some checks are pending
Backend tests / test (push) Waiting to run

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 <noreply@anthropic.com>
This commit is contained in:
goon-foss 2026-10-12 10:14:36 +02:00
parent f6081669a0
commit 0def1a3619
6 changed files with 113 additions and 5 deletions

View file

@ -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',

View file

@ -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();

View file

@ -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();

View file

@ -64,6 +64,33 @@ export async function biometricAvailable(): Promise<boolean> {
return enrolled;
}
/** Czy telefon ma jakakolwiek blokade (odcisk, twarz albo PIN/wzor ekranu). */
export async function ownerAuthAvailable(): Promise<boolean> {
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<boolean> {
try {
const res = await LocalAuthentication.authenticateAsync({
promptMessage: 'Confirm it is you',
disableDeviceFallback: false,
});
return res.success;
} catch {
return false;
}
}
export async function authenticateBiometric(): Promise<boolean> {
const res = await LocalAuthentication.authenticateAsync({
promptMessage: 'Unlock Goon',

View file

@ -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<AppLockSettings | null>(null);
const [bioAvailable, setBioAvailable] = useState(false);
const [ownerAuth, setOwnerAuth] = useState(false);
const [stage, setStage] = useState<Stage>('menu');
const [newPin, setNewPin] = useState('');
const [errorText, setErrorText] = useState<string | null>(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> | 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 (
<View style={styles.center}>
@ -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 (
<PinEntry
key={stage}
title="Enter current PIN"
error={errorText}
onCancel={() => {
@ -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 (
<PinEntry
key={stage}
title="Enter new PIN (4-8 digits)"
error={errorText}
onCancel={() => {
@ -174,6 +211,7 @@ export function AppLockSettingsScreen({ onLogout }: { onLogout?: () => void }) {
if (stage === 'confirm-new') {
return (
<PinEntry
key={stage}
title="Confirm PIN"
error={errorText}
onCancel={() => {
@ -201,6 +239,7 @@ export function AppLockSettingsScreen({ onLogout }: { onLogout?: () => void }) {
if (stage === 'disable-confirm') {
return (
<PinEntry
key={stage}
title="Enter PIN to disable lock"
error={errorText}
onCancel={() => {
@ -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();
})}
/>
);
}

View file

@ -8,11 +8,13 @@ interface Props {
error?: string | null;
onSubmit: (pin: string) => Promise<void> | 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) {
<Text style={styles.primaryText}>Next</Text>
</Pressable>
</View>
{footerAction ? (
<Pressable onPress={footerAction.onPress} hitSlop={10} style={styles.footerAction}>
<Text style={styles.footerActionText}>{footerAction.label}</Text>
</Pressable>
) : null}
</View>
);
}
@ -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 },