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.
This commit is contained in:
parent
bdac60f022
commit
031c309091
5 changed files with 56 additions and 14 deletions
|
|
@ -121,7 +121,12 @@ 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"
|
||||||
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")
|
@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, 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 { 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,7 +257,14 @@ export default function App() {
|
||||||
try {
|
try {
|
||||||
const out = await client.getServerVersion();
|
const out = await client.getServerVersion();
|
||||||
const bundled = APP_VERSION;
|
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"
|
// In-app installer dostępny → "Install" zamiast "Download"
|
||||||
if (apkInstallerAvailable) {
|
if (apkInstallerAvailable) {
|
||||||
const pendingApkUrl = out.apk_url!;
|
const pendingApkUrl = out.apk_url!;
|
||||||
|
|
@ -301,7 +308,11 @@ export default function App() {
|
||||||
};
|
};
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
'Update available',
|
'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: 'Later', style: 'cancel' },
|
||||||
{ text: 'Install', onPress: tryInstall },
|
{ text: 'Install', onPress: tryInstall },
|
||||||
|
|
@ -311,7 +322,7 @@ export default function App() {
|
||||||
// Fallback: Chrome download
|
// Fallback: Chrome download
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
'Update available',
|
'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' },
|
{ text: 'Later', style: 'cancel' },
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -554,7 +554,11 @@ export class GoonClient {
|
||||||
return this.request('/bug-reports/mine/seen', { method: 'POST' });
|
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).
|
// /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.
|
// 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`);
|
const res = await fetch(`${this.baseUrl}/version`);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
* Przy każdym publish OTA: DOPISZ nowy wpis NA GÓRZE (newest-first), z nowym `id`.
|
* 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.
|
* 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 = {
|
export type ChangelogEntry = {
|
||||||
/** Stabilny, rosnący identyfikator (np. data + seq). Newest-first w CHANGELOG. */
|
/** Stabilny, rosnący identyfikator (np. data + seq). Newest-first w CHANGELOG. */
|
||||||
|
|
@ -16,13 +16,16 @@ export type ChangelogEntry = {
|
||||||
/** Punkty zmian, krótkie, user-facing. */
|
/** Punkty zmian, krótkie, user-facing. */
|
||||||
items: string[];
|
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
|
* Po co: przy zmianach natywnych ten sam bundle leci na kanał starej i nowej wersji.
|
||||||
* kanał starej i nowej wersji. Bez tego użytkownik, który już zainstalował 0.3.0,
|
* Bez bramki użytkownik, który już zainstalował nowe APK, dostawał komunikat „pobierz
|
||||||
* dostawał komunikat „pobierz 0.3.0". Wpisy bez tego pola widzą wszyscy.
|
* 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[] = [
|
export const CHANGELOG: ChangelogEntry[] = [
|
||||||
|
|
@ -32,7 +35,7 @@ export const CHANGELOG: ChangelogEntry[] = [
|
||||||
items: [
|
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.',
|
'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',
|
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[] {
|
export function unseenEntries(lastSeenId: string | null): ChangelogEntry[] {
|
||||||
if (!CHANGELOG.length) return [];
|
if (!CHANGELOG.length) return [];
|
||||||
const visible = CHANGELOG.filter(
|
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 (!visible.length) return [];
|
||||||
if (!lastSeenId) return [visible[0]];
|
if (!lastSeenId) return [visible[0]];
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import Constants from 'expo-constants';
|
import Constants from 'expo-constants';
|
||||||
|
import * as Updates from 'expo-updates';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aktualna wersja appki, używana do display'u (BugReportFAB, Update dialog)
|
* 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 =
|
export const APP_VERSION: string =
|
||||||
(Constants.expoConfig?.version as string | undefined) || '0.3.0';
|
(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. */
|
/** „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[] {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue