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 <noreply@anthropic.com>
This commit is contained in:
parent
2cdc3d69da
commit
7d5dd450f7
6 changed files with 74 additions and 15 deletions
23
app/main.py
23
app/main.py
|
|
@ -100,7 +100,7 @@ def healthz() -> dict[str, str]:
|
||||||
|
|
||||||
|
|
||||||
@app.get("/version")
|
@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.
|
"""Mobile sprawdza po starcie żeby wykryć dostępność nowszej wersji APK.
|
||||||
|
|
||||||
Zwraca:
|
Zwraca:
|
||||||
|
|
@ -123,12 +123,21 @@ def version() -> dict[str, str | None]:
|
||||||
# mobile sklei z baseUrl.
|
# mobile sklei z baseUrl.
|
||||||
public_url = os.environ.get("BACKEND_PUBLIC_URL", "").rstrip("/")
|
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"
|
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
|
# `runtime_version` i `apk_build` to jedyne numery, po których aplikacja pozna, JAKIE
|
||||||
# zainstalowane. `version` z app.json przyjeżdża do niej razem z paczką OTA, więc po
|
# APK ma zainstalowane. `version` z app.json przyjeżdża do niej razem z paczką OTA,
|
||||||
# aktualizacji stary APK raportuje nową wersję i sam siebie uznaje za aktualny.
|
# 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).
|
# `apk_build` = `versionCode` z android/app/build.gradle. Runtime sam nie wystarcza:
|
||||||
return {"version": "0.4.1", "runtime_version": "1.4", "apk_url": apk_url}
|
# 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")
|
@app.get("/readyz")
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import { OnboardingModal } from './src/components/OnboardingModal';
|
||||||
import { WhatsNewModal } from './src/components/WhatsNewModal';
|
import { WhatsNewModal } from './src/components/WhatsNewModal';
|
||||||
import { ErrorBoundary } from './src/ErrorBoundary';
|
import { ErrorBoundary } from './src/ErrorBoundary';
|
||||||
import { isAccepted as isAgeGateAccepted } from './src/lib/agegate';
|
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 { DEFAULT_API_KEY, DEFAULT_BACKEND_URL } from './src/lib/backend';
|
||||||
import { getSettings as getLockSettings } from './src/lib/applock';
|
import { getSettings as getLockSettings } from './src/lib/applock';
|
||||||
import { AppNavigator } from './src/navigation';
|
import { AppNavigator } from './src/navigation';
|
||||||
|
|
@ -257,11 +257,18 @@ export default function App() {
|
||||||
try {
|
try {
|
||||||
const out = await client.getServerVersion();
|
const out = await client.getServerVersion();
|
||||||
const bundled = APP_VERSION;
|
const bundled = APP_VERSION;
|
||||||
// Porównujemy RUNTIME, nie numer wersji: `bundled` bierze się z app.json, które
|
// Kolejność źródeł: numer builda APK → runtime → numer wersji. `bundled` bierze
|
||||||
// przyjeżdża razem z paczką OTA, więc stary APK po aktualizacji raportuje nową
|
// się z app.json, które przyjeżdża razem z paczką OTA, więc stary APK po
|
||||||
// wersję i sam siebie uznaje za aktualny. Runtime jest z natywnego manifestu.
|
// aktualizacji raportuje nową wersję i sam siebie uznaje za aktualny. Runtime
|
||||||
// Fallback na porównanie wersji, gdy serwer jeszcze nie podaje runtime.
|
// jest z natywnego manifestu, ale zmienia się tylko przy zmianie kanału OTA:
|
||||||
const needsApk = out.runtime_version && NATIVE_RUNTIME
|
// 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.runtime_version, NATIVE_RUNTIME)
|
||||||
: isNewerVersion(out.version, bundled);
|
: isNewerVersion(out.version, bundled);
|
||||||
if (out.version && needsApk && out.apk_url) {
|
if (out.version && needsApk && out.apk_url) {
|
||||||
|
|
|
||||||
|
|
@ -589,6 +589,8 @@ export class GoonClient {
|
||||||
async getServerVersion(): Promise<{
|
async getServerVersion(): Promise<{
|
||||||
version: string;
|
version: string;
|
||||||
runtime_version?: string | null;
|
runtime_version?: string | null;
|
||||||
|
/** versionCode najnowszego APK; rozróżnia wydania na tym samym runtime. */
|
||||||
|
apk_build?: number | null;
|
||||||
apk_url: string | null;
|
apk_url: string | null;
|
||||||
}> {
|
}> {
|
||||||
// /version nie wymaga API key (publiczny endpoint do upgrade discovery).
|
// /version nie wymaga API key (publiczny endpoint do upgrade discovery).
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,13 @@ export type ChangelogEntry = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CHANGELOG: 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',
|
id: '2026-09-15d',
|
||||||
date: 'September 2026',
|
date: 'September 2026',
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import { captureScreen } from 'react-native-view-shot';
|
||||||
|
|
||||||
import { GoonClient } from '../api';
|
import { GoonClient } from '../api';
|
||||||
import { theme } from '../theme';
|
import { theme } from '../theme';
|
||||||
|
import { nativeBuildNumber } from '../lib/appVersion';
|
||||||
import { BOTTOM_BAR_GAP, BOTTOM_BAR_HEIGHT, barHidden } from '../lib/bottomBar';
|
import { BOTTOM_BAR_GAP, BOTTOM_BAR_HEIGHT, barHidden } from '../lib/bottomBar';
|
||||||
import { formatLastPlayback } from '../lib/lastPlayback';
|
import { formatLastPlayback } from '../lib/lastPlayback';
|
||||||
import { getSxyprnDiag } from '../lib/sxyprnResolver';
|
import { getSxyprnDiag } from '../lib/sxyprnResolver';
|
||||||
|
|
@ -220,6 +221,11 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
|
||||||
? `${pc?.Model || 'android'} / Android ${pc?.Release || Platform.Version}`
|
? `${pc?.Model || 'android'} / Android ${pc?.Release || Platform.Version}`
|
||||||
: `${Platform.OS} ${Platform.Version}`;
|
: `${Platform.OS} ${Platform.Version}`;
|
||||||
extraContext.push(`device=${device.slice(0, 48)}`);
|
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
|
// 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.
|
// się rozjeżdża", bo mówi KTÓRE źródło i gdzie, niezależnie od ekranu zgłoszenia.
|
||||||
const play = formatLastPlayback();
|
const play = formatLastPlayback();
|
||||||
|
|
|
||||||
|
|
@ -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<number | null> {
|
||||||
|
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. */
|
/** „0.3.0" → [0,3,0]. Człony nieliczbowe (np. „0.3.0-rc1") lecą na 0. */
|
||||||
function versionParts(v: string): number[] {
|
function versionParts(v: string): number[] {
|
||||||
return String(v || '').split('.').map((x) => parseInt(x, 10) || 0);
|
return String(v || '').split('.').map((x) => parseInt(x, 10) || 0);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue