From 3b061a58071d4262f673b8d1c5a6e9e85d270e49 Mon Sep 17 00:00:00 2001 From: goon-foss Date: Tue, 6 Oct 2026 16:06:21 +0200 Subject: [PATCH] feat(mobile): ekran "zaktualizuj" dla wersji starszych niz 0.4.x Osobny punkt wejscia (update-required.tsx) z samym ekranem blokady, bez reszty aplikacji i bez przycisku "Pozniej". Publikowany tylko na kanaly OTA starych runtime'ow 1.0-1.3 (0.3.0 to dzis ~8% ruchu): stare APK nie dostaja poprawek natywnych, a ich bledy wracaly w zgloszeniach. Uzywa tylko modulow obecnych w kazdym APK. Runtime 1.0/1.1 (stary klucz podpisu) dostaje kroki z kodem synchronizacji, od 1.2 nowa wersja instaluje sie na wierzchu. publish_update.py --entry podmienia pole main w package.json na czas eksportu i odmawia publikacji na biezacy runtime. Emulator: prawdziwe APK 0.3.0 i 0.2.1 z ta paczka pokazuja ekran, pobieranie otwiera APK w przegladarce, kopiowanie kodu dziala, Wstecz zamyka apke, 0.4.2 instaluje sie na 0.3.0 i startuje normalnie. Co-Authored-By: Claude Opus 5.5 --- mobile/src/UpdateRequired.tsx | 159 ++++++++++++++++++++++++++++++++++ mobile/update-required.tsx | 7 ++ scripts/publish_update.py | 26 +++++- 3 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 mobile/src/UpdateRequired.tsx create mode 100644 mobile/update-required.tsx diff --git a/mobile/src/UpdateRequired.tsx b/mobile/src/UpdateRequired.tsx new file mode 100644 index 0000000..d6d622a --- /dev/null +++ b/mobile/src/UpdateRequired.tsx @@ -0,0 +1,159 @@ +/** + * Ekran "zaktualizuj aplikacje" dla starych APK (runtime 1.0-1.3, wersje sprzed 0.4.0). + * + * To NIE jest czesc zwyklej aplikacji: ta paczka jest budowana z osobnego punktu wejscia + * (`update-required.tsx`, `publish_update.py --entry`) i publikowana tylko na kanaly OTA + * starych runtime'ow. Stare wersje nie dostaja juz poprawek natywnych, a ich bledy wracaly + * w zgloszeniach i Sentry (decyzja Jana 2026-10-06: blokada wszystkiego starszego niz 0.4.x, + * okolo 8% ruchu). Nie ma przycisku "Pozniej" ani zadnej innej funkcji, celowo. + * + * Uzywa wylacznie modulow, ktore sa w kazdym APK od pierwszego wydania (react-native, + * expo-updates, expo-secure-store, expo-clipboard), wiec nie wywali sie na starym kodzie + * natywnym. + * + * Runtime 1.0 i 1.1 (0.2.1 i starsze) maja stary klucz podpisu, ktory przepadl: Android + * nie zainstaluje nowej wersji na wierzchu, trzeba odinstalowac, co czysci telefon. Dla nich + * kroki z kodem synchronizacji, jak w MigrationBanner. Od 1.2 nowa wersja instaluje sie + * na wierzchu i dane zostaja. + */ +import React from 'react'; +import { Linking, Pressable, ScrollView, StatusBar, StyleSheet, Text, View } from 'react-native'; +import * as SecureStore from 'expo-secure-store'; +import * as Updates from 'expo-updates'; + +const VERSION_URL = 'https://api.goon-foss.org/version'; +const APK_FALLBACK = 'https://api.goon-foss.org/static/app-release.apk'; +const SITE_URL = 'https://goon-foss.org/'; +const OLD_KEY_RUNTIMES = ['1.0', '1.1']; + +const C = { + bg: '#14110f', + card: '#1f1a17', + fg: '#f3ede4', + muted: '#a99f94', + accent: '#e05a6d', +}; + +export default function UpdateRequired() { + const runtime = Updates.runtimeVersion || ''; + const oldKey = OLD_KEY_RUNTIMES.includes(runtime); + const [apkUrl, setApkUrl] = React.useState(APK_FALLBACK); + const [latest, setLatest] = React.useState(null); + const [code, setCode] = React.useState(null); + const [copied, setCopied] = React.useState(false); + + React.useEffect(() => { + fetch(VERSION_URL) + .then((r) => r.json()) + .then((v: { version?: string; apk_url?: string | null }) => { + if (v.apk_url && /^https?:\/\//.test(v.apk_url)) setApkUrl(v.apk_url); + if (v.version) setLatest(v.version); + }) + .catch(() => {}); + if (oldKey) { + SecureStore.getItemAsync('goon.device_id') + .then((id) => setCode(id)) + .catch(() => {}); + } + }, [oldKey]); + + const copy = React.useCallback(async () => { + if (!code) return; + try { + const Clipboard = require('expo-clipboard'); + await Clipboard.setStringAsync(code); + setCopied(true); + } catch { + // brak modulu: kod i tak da sie zaznaczyc i skopiowac recznie + } + }, [code]); + + const open = (url: string) => Linking.openURL(url).catch(() => {}); + + return ( + + + + Update required + + This version of Goon is no longer supported and does not get fixes anymore. Install + the new version{latest ? ` (${latest})` : ''} to keep watching. + + + {oldKey ? ( + <> + + Your version was signed with an old key, so Android will not install the new one + over it. Uninstall first, then install the download. That wipes the phone, so copy + your sync code before you start. + + {code ? ( + <> + 1. Copy your sync code + + + {code} + + {copied ? 'Copied' : 'Copy'} + + + ) : null} + {code ? '2.' : '1.'} Download the new version + open(apkUrl)}> + Download the update + + {code ? '3.' : '2.'} Uninstall Goon, then open the file + + Long-press the app icon, Uninstall. Then open the .apk from your notifications or + Downloads. + + {code ? ( + <> + 4. Paste the code back + Settings, Backup & sync, Restore: paste the code, tap Import. + + ) : null} + + ) : ( + <> + open(apkUrl)}> + Download the update + + + Open the downloaded .apk from your notifications or Downloads and tap Install. It + installs over this version, your favourites and settings stay. + + + )} + + open(SITE_URL)} hitSlop={8}> + Download does not start? Open goon-foss.org + + Installed: runtime {runtime || '?'} + + + ); +} + +const s = StyleSheet.create({ + root: { flex: 1, backgroundColor: C.bg }, + content: { padding: 24, paddingTop: 72, paddingBottom: 48 }, + title: { color: C.fg, fontSize: 28, fontWeight: '800', marginBottom: 16 }, + lead: { color: C.fg, fontSize: 16, lineHeight: 23, marginBottom: 14 }, + step: { color: C.fg, fontSize: 16, fontWeight: '700', marginTop: 14, marginBottom: 8 }, + note: { color: C.muted, fontSize: 14, lineHeight: 20, marginTop: 8 }, + codeRow: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: C.card, + borderRadius: 10, + paddingHorizontal: 14, + paddingVertical: 12, + }, + code: { flex: 1, color: C.fg, fontSize: 15, fontFamily: 'monospace' }, + copy: { color: C.accent, fontWeight: '700', marginLeft: 12 }, + btn: { backgroundColor: C.accent, borderRadius: 12, paddingVertical: 15, alignItems: 'center', marginTop: 8 }, + btnText: { color: '#fff', fontSize: 17, fontWeight: '800' }, + link: { color: C.accent, fontSize: 14, marginTop: 28, textAlign: 'center' }, + footer: { color: C.muted, fontSize: 12, marginTop: 24, textAlign: 'center' }, +}); diff --git a/mobile/update-required.tsx b/mobile/update-required.tsx new file mode 100644 index 0000000..2f740bd --- /dev/null +++ b/mobile/update-required.tsx @@ -0,0 +1,7 @@ +// Punkt wejscia paczki "zaktualizuj aplikacje" dla starych runtime'ow (1.0-1.3). +// Zwykla aplikacja startuje z App.tsx; ten plik buduje tylko +// `scripts/publish_update.py --entry update-required.tsx`. Opis w src/UpdateRequired.tsx. +import { registerRootComponent } from 'expo'; +import UpdateRequired from './src/UpdateRequired'; + +registerRootComponent(UpdateRequired); diff --git a/scripts/publish_update.py b/scripts/publish_update.py index f85c408..f2deae9 100644 --- a/scripts/publish_update.py +++ b/scripts/publish_update.py @@ -141,15 +141,39 @@ def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--runtime", default=RUNTIME_DEFAULT) ap.add_argument("--skip-export", action="store_true", help="reuse existing dist/") + ap.add_argument( + "--entry", + help="inny punkt wejscia niz App.tsx, np. update-required.tsx (ekran blokady starych wersji)", + ) args = ap.parse_args() + # Ekran blokady na biezacym kanale zablokowalby wszystkich uzytkownikow. + if args.entry and args.runtime == RUNTIME_DEFAULT: + print(f"ERROR: --entry {args.entry} na biezacym runtime {RUNTIME_DEFAULT} zablokowalby wszystkich. Przerwane.") + return 1 + if not args.skip_export: if DIST.exists(): import shutil shutil.rmtree(DIST) # `--source-maps` dokłada `.hbc.map`, potrzebną do odczytania śladów JS # w Sentry. Mapa NIE jedzie na serwer, kasuje ją `strip_sourcemaps()`. - run(["npx", "expo", "export", "--platform", "android", "--source-maps"], cwd=MOBILE) + # `--entry`: expo export bierze punkt wejscia z pola `main` w package.json, wiec + # podmieniamy je na czas eksportu i zawsze przywracamy. + pkg_path = MOBILE / "package.json" + pkg_orig = pkg_path.read_text(encoding="utf-8") + if args.entry: + if not (MOBILE / args.entry).exists(): + print(f"ERROR: brak pliku mobile/{args.entry}") + return 1 + pkg = json.loads(pkg_orig) + pkg["main"] = args.entry + pkg_path.write_text(json.dumps(pkg, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + try: + run(["npx", "expo", "export", "--platform", "android", "--source-maps"], cwd=MOBILE) + finally: + if args.entry: + pkg_path.write_text(pkg_orig, encoding="utf-8") upload_sourcemaps() strip_sourcemaps()