From 031c309091ef7ea838e5e281cd328ffcddcdb871 Mon Sep 17 00:00:00 2001 From: goon-foss Date: Mon, 24 Aug 2026 14:47:04 +0200 Subject: [PATCH] fix(mobile): stary APK po OTA podawal sie za nowa wersje i nie dostawal monitu Zgloszone przez uzytkownika: po wydaniu 0.3.0 nie dostal propozycji aktualizacji. Przyczyna jest moja. `APP_VERSION` czyta `Constants.expoConfig.version`, czyli app.json, a app.json przyjezdza do aplikacji RAZEM Z PACZKA OTA. Podbilem wersje na 0.3.0 i opublikowalem te paczke rowniez na stary kanal 1.2, wiec kazdy uzytkownik APK 0.2.2 zaczal raportowac 0.3.0 i sam siebie uznal za aktualnego. Skutek podwojny: nie dostal ani monitu o nowym APK, ani wpisu w changelogu, ktory byl kierowany dokladnie do niego. Poprzednia poprawka (dad4bbf, porownanie "nowsza" zamiast "rozna") tego NIE naprawiala: przy dwoch identycznych numerach oba warunki daja falsz. Jedyny numer odporny na OTA to runtime z AndroidManifest, bo expo-updates uzywa go do sprawdzania zgodnosci paczek i nie moze brac go z paczki. Stad `NATIVE_RUNTIME` w `src/lib/appVersion.ts`, a serwer oddaje teraz `runtime_version` obok `version`. Porownanie leci po runtime, z zapasowym porownaniem wersji, gdy serwer go nie poda. Bramka wpisow w changelogu przeszla z `onlyBelowVersion` na `onlyBelowRuntime`. `expo-application` bylby czystszym zrodlem natywnej wersji, ale to modul natywny, wiec nie wszedlby przez OTA do ludzi na starym APK, czyli dokladnie do tych, ktorych to dotyczy. Runtime dziala na tym, co juz maja zainstalowane. Przy okazji tresc dialogu: pokazywala "Server: 0.3.0, App: 0.3.0", czyli dwie identyczne liczby, bo drugi numer byl z paczki, a nie z APK. Teraz nie pokazujemy numeru zainstalowanej wersji, bo po OTA nie mamy wiarygodnego. Zweryfikowane na emulatorze w obie strony, na podpisanych APK: * APK 0.2.2 (runtime 1.2) po pobraniu paczki: monit jest, tresc czytelna. * APK 0.3.0 (runtime 1.3): monitu nie ma. Instalacja 0.3.0 na wierzch 0.2.2 przeszla ponownie. --- app/main.py | 7 ++++++- mobile/App.tsx | 19 +++++++++++++++---- mobile/src/api.ts | 6 +++++- mobile/src/changelog.ts | 19 +++++++++++-------- mobile/src/lib/appVersion.ts | 19 +++++++++++++++++++ 5 files changed, 56 insertions(+), 14 deletions(-) diff --git a/app/main.py b/app/main.py index 2a645a4..d79b075 100644 --- a/app/main.py +++ b/app/main.py @@ -121,7 +121,12 @@ def version() -> dict[str, str | None]: # mobile sklei z baseUrl. public_url = os.environ.get("BACKEND_PUBLIC_URL", "").rstrip("/") apk_url = f"{public_url}/static/app-release.apk" if public_url else "/static/app-release.apk" - return {"version": "0.3.0", "apk_url": apk_url} + # `runtime_version` to jedyny numer, po którym aplikacja pozna, JAKIE APK ma + # zainstalowane. `version` z app.json przyjeżdża do niej razem z paczką OTA, więc po + # aktualizacji stary APK raportuje nową wersję i sam siebie uznaje za aktualny. + # Runtime siedzi w AndroidManifest, OTA go nie rusza. Podbijaj RAZEM z `version`, + # ale tylko przy wydaniu APK (zmiana natywna). + return {"version": "0.3.0", "runtime_version": "1.3", "apk_url": apk_url} @app.get("/readyz") diff --git a/mobile/App.tsx b/mobile/App.tsx index 254d8d9..deadae7 100644 --- a/mobile/App.tsx +++ b/mobile/App.tsx @@ -32,7 +32,7 @@ import { OnboardingModal } from './src/components/OnboardingModal'; import { WhatsNewModal } from './src/components/WhatsNewModal'; import { ErrorBoundary } from './src/ErrorBoundary'; import { isAccepted as isAgeGateAccepted } from './src/lib/agegate'; -import { APP_VERSION, isNewerVersion } from './src/lib/appVersion'; +import { APP_VERSION, NATIVE_RUNTIME, isNewerVersion } from './src/lib/appVersion'; import { DEFAULT_API_KEY, DEFAULT_BACKEND_URL } from './src/lib/backend'; import { getSettings as getLockSettings } from './src/lib/applock'; import { AppNavigator } from './src/navigation'; @@ -257,7 +257,14 @@ export default function App() { try { const out = await client.getServerVersion(); const bundled = APP_VERSION; - if (out.version && isNewerVersion(out.version, bundled) && out.apk_url) { + // Porównujemy RUNTIME, nie numer wersji: `bundled` bierze się z app.json, które + // przyjeżdża razem z paczką OTA, więc stary APK po aktualizacji raportuje nową + // wersję i sam siebie uznaje za aktualny. Runtime jest z natywnego manifestu. + // Fallback na porównanie wersji, gdy serwer jeszcze nie podaje runtime. + const needsApk = out.runtime_version && NATIVE_RUNTIME + ? isNewerVersion(out.runtime_version, NATIVE_RUNTIME) + : isNewerVersion(out.version, bundled); + if (out.version && needsApk && out.apk_url) { // In-app installer dostępny → "Install" zamiast "Download" if (apkInstallerAvailable) { const pendingApkUrl = out.apk_url!; @@ -301,7 +308,11 @@ export default function App() { }; Alert.alert( 'Update available', - `Server: ${out.version}\nApp: ${bundled}\n\nInstall update now?`, + // NIE pokazujemy tu numeru wersji aplikacji. Po aktualizacji OTA bierze się + // on z manifestu paczki, a nie z zainstalowanego APK, więc na starym APK + // wyświetlały się dwie identyczne liczby („Server: 0.3.0, App: 0.3.0") i + // monit wyglądał na pomyłkę. + `Goon ${out.version} is ready. It adds parts that silent updates cannot deliver, so it installs like a normal app.\n\nInstall now?`, [ { text: 'Later', style: 'cancel' }, { text: 'Install', onPress: tryInstall }, @@ -311,7 +322,7 @@ export default function App() { // Fallback: Chrome download Alert.alert( 'Update available', - `Server: ${out.version}\nApp: ${bundled}\n\nTap "Download" to install the latest APK.`, + `Goon ${out.version} is ready. It adds parts that silent updates cannot deliver, so you install it yourself.\n\nTap "Download" to get the APK.`, [ { text: 'Later', style: 'cancel' }, { diff --git a/mobile/src/api.ts b/mobile/src/api.ts index 7f69f2b..1c3b75f 100644 --- a/mobile/src/api.ts +++ b/mobile/src/api.ts @@ -554,7 +554,11 @@ export class GoonClient { return this.request('/bug-reports/mine/seen', { method: 'POST' }); } - async getServerVersion(): Promise<{ version: string; apk_url: string | null }> { + async getServerVersion(): Promise<{ + version: string; + runtime_version?: string | null; + apk_url: string | null; + }> { // /version nie wymaga API key (publiczny endpoint do upgrade discovery). // Używamy this.request bo czysty fetch bez API key też jest OK, ale endpoint jest auth-free. const res = await fetch(`${this.baseUrl}/version`); diff --git a/mobile/src/changelog.ts b/mobile/src/changelog.ts index 03b3717..0f0e051 100644 --- a/mobile/src/changelog.ts +++ b/mobile/src/changelog.ts @@ -6,7 +6,7 @@ * Przy każdym publish OTA: DOPISZ nowy wpis NA GÓRZE (newest-first), z nowym `id`. * Treść user-facing (bez wewnętrznego żargonu), to widzi użytkownik. */ -import { APP_VERSION, isNewerVersion } from './lib/appVersion'; +import { NATIVE_RUNTIME, isNewerVersion } from './lib/appVersion'; export type ChangelogEntry = { /** Stabilny, rosnący identyfikator (np. data + seq). Newest-first w CHANGELOG. */ @@ -16,13 +16,16 @@ export type ChangelogEntry = { /** Punkty zmian, krótkie, user-facing. */ items: string[]; /** - * Pokaż ten wpis tylko użytkownikom PONIŻEJ tej wersji aplikacji. + * Pokaż ten wpis tylko użytkownikom, których APK ma runtime STARSZY niż podany. * - * Po co: przy zmianach wymagających nowego APK (natywnych) ten sam bundle leci na - * kanał starej i nowej wersji. Bez tego użytkownik, który już zainstalował 0.3.0, - * dostawał komunikat „pobierz 0.3.0". Wpisy bez tego pola widzą wszyscy. + * Po co: przy zmianach natywnych ten sam bundle leci na kanał starej i nowej wersji. + * Bez bramki użytkownik, który już zainstalował nowe APK, dostawał komunikat „pobierz + * nowe APK". Bramkujemy po runtime, a nie po numerze wersji, bo numer wersji przyjeżdża + * razem z paczką OTA i stary APK po aktualizacji podaje już nowy (przez to wpis ukrywał + * się przed dokładnie tymi ludźmi, do których był kierowany). Wpisy bez tego pola widzą + * wszyscy. */ - onlyBelowVersion?: string; + onlyBelowRuntime?: string; }; export const CHANGELOG: ChangelogEntry[] = [ @@ -32,7 +35,7 @@ export const CHANGELOG: ChangelogEntry[] = [ items: [ 'Chromecast is here, but it needs version 0.3.0, which you have to download and install yourself. Casting needs a part of the app that cannot be delivered as a silent update. Get it from goon-foss.org; it installs over the version you already have and keeps your settings.', ], - onlyBelowVersion: '0.3.0', + onlyBelowRuntime: '1.3', }, { id: '2026-08-24a', @@ -579,7 +582,7 @@ export const NEWEST_CHANGELOG_ID = CHANGELOG.length ? CHANGELOG[0].id : null; export function unseenEntries(lastSeenId: string | null): ChangelogEntry[] { if (!CHANGELOG.length) return []; const visible = CHANGELOG.filter( - (e) => !e.onlyBelowVersion || isNewerVersion(e.onlyBelowVersion, APP_VERSION), + (e) => !e.onlyBelowRuntime || isNewerVersion(e.onlyBelowRuntime, NATIVE_RUNTIME || '0'), ); if (!visible.length) return []; if (!lastSeenId) return [visible[0]]; diff --git a/mobile/src/lib/appVersion.ts b/mobile/src/lib/appVersion.ts index a2ca123..d761c76 100644 --- a/mobile/src/lib/appVersion.ts +++ b/mobile/src/lib/appVersion.ts @@ -1,4 +1,5 @@ import Constants from 'expo-constants'; +import * as Updates from 'expo-updates'; /** * Aktualna wersja appki, używana do display'u (BugReportFAB, Update dialog) @@ -19,6 +20,24 @@ import Constants from 'expo-constants'; export const APP_VERSION: string = (Constants.expoConfig?.version as string | undefined) || '0.3.0'; +/** + * Runtime zapisany w APK (`EXPO_RUNTIME_VERSION` w AndroidManifest). JEDYNY numer, po + * którym aplikacja pozna, jaką paczkę natywną ma naprawdę zainstalowaną. + * + * Po co osobno, skoro jest `APP_VERSION`: ta bierze się z `app.json`, a `app.json` + * przyjeżdża do aplikacji RAZEM Z PACZKĄ OTA. Po podbiciu wersji na 0.3.0 i wysłaniu tej + * paczki na stary kanał każdy użytkownik APK 0.2.2 zaczął raportować 0.3.0 i sam siebie + * uznał za aktualnego: nie dostał ani monitu o nowym APK, ani wpisu w changelogu, który + * właśnie do niego był kierowany. Runtime siedzi w natywnym manifeście i OTA go nie rusza. + */ +export const NATIVE_RUNTIME: string | null = (() => { + try { + return Updates.runtimeVersion ?? null; + } catch { + return null; + } +})(); + /** „0.3.0" → [0,3,0]. Człony nieliczbowe (np. „0.3.0-rc1") lecą na 0. */ function versionParts(v: string): number[] {