From 7d5dd450f7f56d2ee17ae71a8dd4a98e73cf0ccf Mon Sep 17 00:00:00 2001 From: goon-foss Date: Wed, 16 Sep 2026 09:45:40 +0200 Subject: [PATCH] fix(mobile): monit o nowe APK takze na tym samym kanale OTA 0.4.0 i 0.4.1 maja ten sam runtime 1.4, a monit porownywal tylko runtime, wiec nikt z 0.4.0 nie dostal propozycji aktualizacji i crash expo-video przy zamykaniu z ostatnich wracal do Sentry dzien po poprawce (147164135). - /version oddaje apk_build (versionCode 15) - aplikacja czyta versionCode ZAINSTALOWANEGO APK przez natywny modul Sentry (expo-constants w SDK 52 nie oddaje go wcale, expo-application wymagalby nowego APK) - numer builda trafia tez do kontekstu zgloszen z apki: app_version jedzie z paczka OTA, wiec nie mowil, ktore wydanie natywne ma zglaszajacy Sprawdzone na emulatorze: 0.4.1 nie dostaje monitu i raportuje build=15, po instalacji 0.4.0 monit "Update available" pokazuje sie przy starcie. Co-Authored-By: Claude Opus 5 --- app/main.py | 23 ++++++++++++++------- mobile/App.tsx | 23 +++++++++++++-------- mobile/src/api.ts | 2 ++ mobile/src/changelog.ts | 7 +++++++ mobile/src/components/BugReportFAB.tsx | 6 ++++++ mobile/src/lib/appVersion.ts | 28 ++++++++++++++++++++++++++ 6 files changed, 74 insertions(+), 15 deletions(-) diff --git a/app/main.py b/app/main.py index a6b0ecb..ba76ca1 100644 --- a/app/main.py +++ b/app/main.py @@ -100,7 +100,7 @@ def healthz() -> dict[str, str]: @app.get("/version") -def version() -> dict[str, str | None]: +def version() -> dict[str, str | int | None]: """Mobile sprawdza po starcie żeby wykryć dostępność nowszej wersji APK. Zwraca: @@ -123,12 +123,21 @@ 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" - # `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.4.1", "runtime_version": "1.4", "apk_url": apk_url} + # `runtime_version` i `apk_build` to jedyne numery, po których 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. + # + # `apk_build` = `versionCode` z android/app/build.gradle. Runtime sam nie wystarcza: + # 0.4.0 i 0.4.1 mają ten sam runtime 1.4 (0.4.1 to poprawki natywne bez zmiany kanału + # OTA), więc przez dobę nikt z 0.4.0 nie dostał monitu, mimo że 0.4.1 naprawia crash + # expo-video przy zamykaniu z ostatnich (Sentry 147164135, zgłoszenie 2026-09-15). + # Podbijaj RAZEM z `version` przy każdym wydaniu APK. + return { + "version": "0.4.1", + "runtime_version": "1.4", + "apk_build": 15, + "apk_url": apk_url, + } @app.get("/readyz") diff --git a/mobile/App.tsx b/mobile/App.tsx index deadae7..ad8e99a 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, NATIVE_RUNTIME, isNewerVersion } from './src/lib/appVersion'; +import { APP_VERSION, NATIVE_RUNTIME, isNewerVersion, nativeBuildNumber } 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,13 +257,20 @@ export default function App() { try { const out = await client.getServerVersion(); const bundled = APP_VERSION; - // 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); + // Kolejność źródeł: numer builda APK → runtime → 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, ale zmienia się tylko przy zmianie kanału OTA: + // 0.4.0 i 0.4.1 mają ten sam runtime 1.4, więc wydanie naprawcze nie docierało + // do nikogo (crash expo-video z 0.4.0 wracał do Sentry dzień po poprawce). + // Numer builda rozróżnia nawet dwa APK na tym samym kanale. + const nativeBuild = await nativeBuildNumber(); + const serverBuild = typeof out.apk_build === 'number' ? out.apk_build : null; + const needsApk = serverBuild && nativeBuild + ? serverBuild > nativeBuild + : 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) { diff --git a/mobile/src/api.ts b/mobile/src/api.ts index a70048a..5031b47 100644 --- a/mobile/src/api.ts +++ b/mobile/src/api.ts @@ -589,6 +589,8 @@ export class GoonClient { async getServerVersion(): Promise<{ version: string; runtime_version?: string | null; + /** versionCode najnowszego APK; rozróżnia wydania na tym samym runtime. */ + apk_build?: number | null; apk_url: string | null; }> { // /version nie wymaga API key (publiczny endpoint do upgrade discovery). diff --git a/mobile/src/changelog.ts b/mobile/src/changelog.ts index bdab507..c3e5157 100644 --- a/mobile/src/changelog.ts +++ b/mobile/src/changelog.ts @@ -35,6 +35,13 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + id: '2026-09-16a', + date: 'September 2026', + items: [ + 'The app now notices when a newer build is out even if it shares the silent-update channel with yours, so fixes that need a real install stop waiting for you to find them. If you are on 0.4.0, expect a prompt for 0.4.1: it fixes a crash when closing the app from recents.', + ], + }, { id: '2026-09-15d', date: 'September 2026', diff --git a/mobile/src/components/BugReportFAB.tsx b/mobile/src/components/BugReportFAB.tsx index 70c2db3..5b32073 100644 --- a/mobile/src/components/BugReportFAB.tsx +++ b/mobile/src/components/BugReportFAB.tsx @@ -36,6 +36,7 @@ import { captureScreen } from 'react-native-view-shot'; import { GoonClient } from '../api'; import { theme } from '../theme'; +import { nativeBuildNumber } from '../lib/appVersion'; import { BOTTOM_BAR_GAP, BOTTOM_BAR_HEIGHT, barHidden } from '../lib/bottomBar'; import { formatLastPlayback } from '../lib/lastPlayback'; import { getSxyprnDiag } from '../lib/sxyprnResolver'; @@ -220,6 +221,11 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) { ? `${pc?.Model || 'android'} / Android ${pc?.Release || Platform.Version}` : `${Platform.OS} ${Platform.Version}`; extraContext.push(`device=${device.slice(0, 48)}`); + // Numer builda ZAINSTALOWANEGO APK. `app_version` idzie z paczki OTA, wiec zgloszenie + // z APK 0.4.0 po aktualizacji podawalo 0.4.1 i nie dalo sie odroznic, kto ma ktore + // wydanie natywne (crash expo-video siedzial tylko w 0.4.0). + const build = await nativeBuildNumber(); + if (build) extraContext.push(`build=${build}`); // Ostatnie odtwarzanie (serwer/host/pozycja/błąd), kluczowe dla "nie gra"/"audio // się rozjeżdża", bo mówi KTÓRE źródło i gdzie, niezależnie od ekranu zgłoszenia. const play = formatLastPlayback(); diff --git a/mobile/src/lib/appVersion.ts b/mobile/src/lib/appVersion.ts index eb76c15..b7db891 100644 --- a/mobile/src/lib/appVersion.ts +++ b/mobile/src/lib/appVersion.ts @@ -39,6 +39,34 @@ export const NATIVE_RUNTIME: string | null = (() => { })(); +/** + * `versionCode` ZAINSTALOWANEGO APK (0.4.0 = 14, 0.4.1 = 15), albo null. + * + * Po co, skoro jest runtime: runtime zmienia się tylko przy zmianie kanału OTA, a wydanie + * naprawcze zostaje na tym samym kanale. 0.4.0 i 0.4.1 mają oba runtime 1.4, więc nikt + * z 0.4.0 nie dostał monitu o 0.4.1, mimo że tamto APK naprawia crash przy zamykaniu + * aplikacji z ostatnich (Sentry 147164135). Numer builda rozróżnia je bez podbijania + * runtime i bez utrzymywania kolejnego kanału OTA. + * + * Skąd: natywny moduł Sentry, który i tak jest w aplikacji i czyta PackageInfo (to stąd + * bierze się `app_build` w zdarzeniach). expo-constants w SDK 52 oddaje puste + * `platform.android`, a expo-application wymagałby nowego APK, czyli dokładnie tego, + * czego tu brakuje. Wejście prywatne, więc całość w try/catch: brak numeru = stare + * zachowanie (porównanie runtime). + */ +export async function nativeBuildNumber(): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { NATIVE } = require('@sentry/react-native/dist/js/wrapper'); + const ctx: any = await NATIVE.fetchNativeDeviceContexts(); + const raw = ctx?.contexts?.app?.app_build ?? ctx?.app?.app_build; + const n = parseInt(String(raw ?? ''), 10); + return Number.isFinite(n) && n > 0 ? n : 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[] { return String(v || '').split('.').map((x) => parseInt(x, 10) || 0);