fix(ota+mobile): strip expo-font from bundle, runtime back to 1.0

DIAGNOZA NA EMULATORZE (emulator-5554, goon-v0.1.9.apk):
Dwa błędne założenia z poprzednich sesji obalone empirycznie:

1. RUNTIME: APK ma EXPO_RUNTIME_VERSION="1.0" (NIE 0.1.9 — pomyliłem versionName
   z runtime). App akceptuje TYLKO manifest runtime 1.0. Mój wcześniejszy
   "fix" na 0.1.9 (c19da51) był wstecz — app go ignorował. Cofnięte: app.json
   + publish_update RUNTIME_DEFAULT z powrotem na "1.0".

2. CRASH: prawdziwa przyczyna "nic się nie pojawia" — OTA bundle z expo-font
   crashował: "Cannot find native module 'ExpoFontLoader'" → expo-updates
   ErrorRecovery rollback. APK (build 22-maja) nie ma natywnego ExpoFontLoader
   (expo-font dodany 30-maja, PO buildzie APK). OTA NIE MOŻE dostarczyć native
   modułu. Potwierdzone: embedded bundle + served bundle grep = 0 ExpoFontLoader;
   stary font-bundle crashował, font-stripped NIE.

FIX: usunięto useFonts z App.tsx + expo-font import; theme.fonts → undefined
(system font); SceneTile/MoviePosterCard/navigation/GoonWordmark fontFamily →
fontWeight. Wszystko inne (2-col grid, oxblood, logo SVG-RNSVG-jest-w-APK)
zostaje. Custom fonty wrócą przy rebuildzie APK z expo-font (option B).

ZWERYFIKOWANE: bundle d5b87e5c (runtime 1.0, 0 ttf) — emulator launch:
`ReactNativeJS: Running "main"`, zero JS errors, brak ExpoFontLoader crash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jtrzupek 2026-05-31 11:41:29 +02:00
parent 05c0f6ef93
commit 64506690df
8 changed files with 29 additions and 44 deletions

View file

@ -6,8 +6,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import * as Sentry from '@sentry/react-native'; import * as Sentry from '@sentry/react-native';
import Constants from 'expo-constants'; import Constants from 'expo-constants';
import { registerRootComponent } from 'expo'; import { registerRootComponent } from 'expo';
import { useFonts } from 'expo-font';
import { Text as RNText } from 'react-native';
import * as ScreenCapture from 'expo-screen-capture'; import * as ScreenCapture from 'expo-screen-capture';
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import * as Updates from 'expo-updates'; import * as Updates from 'expo-updates';
@ -77,30 +75,12 @@ const queryClient = new QueryClient({
}, },
}); });
// Globalny default fontu dla całego <Text> — General Sans Regular jako body. // NB: custom fonty (General Sans + Geist Mono) USUNIĘTE z bundla 2026-05-31 —
// Komponenty które chcą display/mono nadpisują fontFamily jawnie (bo RN nie // `expo-font`/`ExpoFontLoader` natywny moduł NIE jest w APK 0.1.9 (build 22-maja,
// syntezuje weightów dla custom fontów). Bold-ale-bez-fontFamily tekst zostanie // przed dodaniem expo-font) → useFonts crashował OTA bundle (ErrorRecovery rollback).
// Regular weightem General Sans — wciąż distinctive face, akceptowalne dla // System font do czasu rebuildu APK z expo-font. Patrz [[reference-ota-runtime-version]].
// nietkniętych ekranów; high-traffic komponenty mają jawny Semibold.
let _textDefaultApplied = false;
function applyDefaultFont() {
if (_textDefaultApplied) return;
_textDefaultApplied = true;
const T = RNText as unknown as { defaultProps?: { style?: unknown } };
T.defaultProps = T.defaultProps || {};
const prev = T.defaultProps.style;
T.defaultProps.style = [{ fontFamily: 'GeneralSans-Regular' }, prev].filter(Boolean);
}
export default function App() { export default function App() {
const [fontsLoaded] = useFonts({
'GeneralSans-Regular': require('./assets/fonts/GeneralSans-Regular.ttf'),
'GeneralSans-Medium': require('./assets/fonts/GeneralSans-Medium.ttf'),
'GeneralSans-Semibold': require('./assets/fonts/GeneralSans-Semibold.ttf'),
'GeistMono-Regular': require('./assets/fonts/GeistMono-Regular.ttf'),
});
if (fontsLoaded) applyDefaultFont();
const [hydrated, setHydrated] = useState(false); const [hydrated, setHydrated] = useState(false);
const [ageAccepted, setAgeAccepted] = useState(false); const [ageAccepted, setAgeAccepted] = useState(false);
const [client, setClient] = useState<GoonClient | null>(null); const [client, setClient] = useState<GoonClient | null>(null);
@ -270,7 +250,7 @@ export default function App() {
return () => sub.remove(); return () => sub.remove();
}, []); }, []);
if (!fontsLoaded || !hydrated || !lockReady) { if (!hydrated || !lockReady) {
return ( return (
<View style={{ flex: 1, backgroundColor: theme.bg, justifyContent: 'center' }}> <View style={{ flex: 1, backgroundColor: theme.bg, justifyContent: 'center' }}>
<ActivityIndicator color={theme.fg} /> <ActivityIndicator color={theme.fg} />

View file

@ -6,7 +6,7 @@
"orientation": "portrait", "orientation": "portrait",
"userInterfaceStyle": "automatic", "userInterfaceStyle": "automatic",
"newArchEnabled": false, "newArchEnabled": false,
"runtimeVersion": "0.1.9", "runtimeVersion": "1.0",
"updates": { "updates": {
"enabled": true, "enabled": true,
"url": "https://api.goon-foss.org/expo-updates/manifest", "url": "https://api.goon-foss.org/expo-updates/manifest",

View file

@ -17,7 +17,7 @@ import React from 'react';
import { Text, View } from 'react-native'; import { Text, View } from 'react-native';
import Svg, { Circle } from 'react-native-svg'; import Svg, { Circle } from 'react-native-svg';
import { fonts, theme } from '../theme'; import { theme } from '../theme';
interface WordmarkProps { interface WordmarkProps {
/** fontSize wordmarku w px. */ /** fontSize wordmarku w px. */
@ -29,8 +29,9 @@ interface WordmarkProps {
} }
export function GoonWordmark({ size = 26, color = theme.fg, mono = false }: WordmarkProps) { export function GoonWordmark({ size = 26, color = theme.fg, mono = false }: WordmarkProps) {
// System bold (custom font usunięty z OTA bundla — ExpoFontLoader nie w APK).
const base = { const base = {
fontFamily: fonts.display, fontWeight: '800' as const,
fontSize: size, fontSize: size,
letterSpacing: -size * 0.03, letterSpacing: -size * 0.03,
includeFontPadding: false as const, includeFontPadding: false as const,

View file

@ -3,7 +3,7 @@
import { Image } from 'expo-image'; import { Image } from 'expo-image';
import React from 'react'; import React from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native'; import { Pressable, StyleSheet, Text, View } from 'react-native';
import { fonts, theme } from '../theme'; import { theme } from '../theme';
import type { MovieOut } from '../types'; import type { MovieOut } from '../types';
export function MoviePosterCard({ export function MoviePosterCard({
@ -101,7 +101,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 6, paddingHorizontal: 6,
paddingVertical: 2, paddingVertical: 2,
}, },
newBadgeText: { color: theme.fg, fontSize: 9, fontFamily: fonts.mono, fontWeight: '700', letterSpacing: 0.6 }, newBadgeText: { color: theme.fg, fontSize: 9, fontWeight: '800', letterSpacing: 0.6 },
posterDimmed: { opacity: 0.45 }, posterDimmed: { opacity: 0.45 },
watchedBadge: { watchedBadge: {
position: 'absolute', position: 'absolute',
@ -124,7 +124,7 @@ const styles = StyleSheet.create({
backgroundColor: 'rgba(0,0,0,0.5)', backgroundColor: 'rgba(0,0,0,0.5)',
}, },
progressFg: { height: 3, backgroundColor: theme.accent }, progressFg: { height: 3, backgroundColor: theme.accent },
title: { color: theme.fg, fontSize: 13, fontFamily: fonts.display, marginTop: 6, letterSpacing: -0.2 }, title: { color: theme.fg, fontSize: 13, fontWeight: '600', marginTop: 6, letterSpacing: -0.2 },
titleDimmed: { color: theme.muted }, titleDimmed: { color: theme.muted },
meta: { color: theme.muted, fontSize: 10, fontFamily: fonts.mono, marginTop: 2, letterSpacing: 0.5, textTransform: 'uppercase' }, meta: { color: theme.muted, fontSize: 10, fontWeight: '600', marginTop: 2, letterSpacing: 0.5, textTransform: 'uppercase' },
}); });

View file

@ -27,7 +27,7 @@ import React, { useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native'; import { Pressable, StyleSheet, Text, View } from 'react-native';
import type { RootStackParamList } from '../navigation'; import type { RootStackParamList } from '../navigation';
import { fonts, theme } from '../theme'; import { theme } from '../theme';
import type { SceneOut } from '../types'; import type { SceneOut } from '../types';
import { Thumb } from './Thumb'; import { Thumb } from './Thumb';
@ -190,13 +190,13 @@ const styles = StyleSheet.create({
durText: { durText: {
color: theme.fg, color: theme.fg,
fontSize: 11, fontSize: 11,
fontFamily: fonts.mono, fontWeight: '600',
fontVariant: ['tabular-nums'], fontVariant: ['tabular-nums'],
}, },
title: { title: {
color: theme.fg, color: theme.fg,
fontSize: 14, fontSize: 14,
fontFamily: fonts.display, fontWeight: '600',
marginTop: 8, marginTop: 8,
letterSpacing: -0.2, letterSpacing: -0.2,
}, },
@ -204,7 +204,7 @@ const styles = StyleSheet.create({
meta: { meta: {
color: theme.muted, color: theme.muted,
fontSize: 10, fontSize: 10,
fontFamily: fonts.mono, fontWeight: '600',
marginTop: 3, marginTop: 3,
letterSpacing: 0.5, letterSpacing: 0.5,
textTransform: 'uppercase', textTransform: 'uppercase',

View file

@ -24,7 +24,7 @@ import { SitesScreen } from './screens/SitesScreen';
import { StudioScenesScreen } from './screens/StudioScenesScreen'; import { StudioScenesScreen } from './screens/StudioScenesScreen';
import { TagScenesScreen } from './screens/TagScenesScreen'; import { TagScenesScreen } from './screens/TagScenesScreen';
import { TagsScreen } from './screens/TagsScreen'; import { TagsScreen } from './screens/TagsScreen';
import { fonts, theme } from './theme'; import { theme } from './theme';
export type RootStackParamList = { export type RootStackParamList = {
Scenes: undefined; Scenes: undefined;
@ -101,7 +101,7 @@ function TopTabs({
style={{ style={{
color: active ? theme.accent : theme.muted, color: active ? theme.accent : theme.muted,
fontSize: 13, fontSize: 13,
fontFamily: active ? fonts.display : fonts.medium, fontWeight: active ? '700' : '500',
letterSpacing: 0.3, letterSpacing: 0.3,
}} }}
> >
@ -145,7 +145,7 @@ export function AppNavigator({ onLogout, client, appVersion }: AppNavigatorProps
<Stack.Navigator <Stack.Navigator
screenOptions={{ screenOptions={{
headerStyle: { backgroundColor: theme.card }, headerStyle: { backgroundColor: theme.card },
headerTitleStyle: { color: theme.fg, fontFamily: fonts.display }, headerTitleStyle: { color: theme.fg },
headerTintColor: theme.accent, headerTintColor: theme.accent,
contentStyle: { backgroundColor: theme.bg }, contentStyle: { backgroundColor: theme.bg },
}} }}

View file

@ -56,11 +56,15 @@ export const theme = {
* Gdy useFonts jeszcze nie ready, App.tsx blokuje render (fonty z bundla * Gdy useFonts jeszcze nie ready, App.tsx blokuje render (fonty z bundla
* ładują się <100ms), więc te stałe zawsze valid przy pierwszym paint. * ładują się <100ms), więc te stałe zawsze valid przy pierwszym paint.
*/ */
// Custom fonty USUNIĘTE z OTA bundla 2026-05-31 (ExpoFontLoader native nie ma w
// APK 0.1.9 → crash). `undefined` = system font (San Francisco/Roboto). Komponenty
// fallbackują na fontWeight dla hierarchii. Przywrócić do realnych rodzin gdy
// nowy APK z expo-font wejdzie do dystrybucji. Patrz [[reference-ota-runtime-version]].
export const fonts = { export const fonts = {
body: 'GeneralSans-Regular', body: undefined as string | undefined,
medium: 'GeneralSans-Medium', medium: undefined as string | undefined,
display: 'GeneralSans-Semibold', display: undefined as string | undefined,
mono: 'GeistMono-Regular', mono: undefined as string | undefined,
} as const; } as const;
export function scoreColor(score: number): string { export function scoreColor(score: number): string {

View file

@ -29,7 +29,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent ROOT = Path(__file__).resolve().parent.parent
MOBILE = ROOT / "mobile" MOBILE = ROOT / "mobile"
DIST = MOBILE / "dist" DIST = MOBILE / "dist"
RUNTIME_DEFAULT = "0.1.9" # MUSI == EXPO_RUNTIME_VERSION w APK (AndroidManifest). Zweryfikowane 2026-05-31. RUNTIME_DEFAULT = "1.0" # == EXPO_RUNTIME_VERSION w APK (zweryfikowane emulatorem 2026-05-31: app akceptuje TYLKO runtime 1.0).
# Operator config — set in your shell / .env.local before running this script. # Operator config — set in your shell / .env.local before running this script.
# Defaults are placeholders intended to fail loudly if you forgot to configure. # Defaults are placeholders intended to fail loudly if you forgot to configure.
VPS = os.environ.get("GOON_VPS_SSH", "root@your-vps.example.com") VPS = os.environ.get("GOON_VPS_SSH", "root@your-vps.example.com")