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 <noreply@anthropic.com>
This commit is contained in:
parent
7e48cdee06
commit
3b061a5807
3 changed files with 191 additions and 1 deletions
159
mobile/src/UpdateRequired.tsx
Normal file
159
mobile/src/UpdateRequired.tsx
Normal file
|
|
@ -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<string | null>(null);
|
||||||
|
const [code, setCode] = React.useState<string | null>(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 (
|
||||||
|
<View style={s.root}>
|
||||||
|
<StatusBar barStyle="light-content" backgroundColor={C.bg} />
|
||||||
|
<ScrollView contentContainerStyle={s.content}>
|
||||||
|
<Text style={s.title}>Update required</Text>
|
||||||
|
<Text style={s.lead}>
|
||||||
|
This version of Goon is no longer supported and does not get fixes anymore. Install
|
||||||
|
the new version{latest ? ` (${latest})` : ''} to keep watching.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{oldKey ? (
|
||||||
|
<>
|
||||||
|
<Text style={s.lead}>
|
||||||
|
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.
|
||||||
|
</Text>
|
||||||
|
{code ? (
|
||||||
|
<>
|
||||||
|
<Text style={s.step}>1. Copy your sync code</Text>
|
||||||
|
<Pressable style={s.codeRow} onPress={copy}>
|
||||||
|
<Text style={s.code} selectable numberOfLines={1}>
|
||||||
|
{code}
|
||||||
|
</Text>
|
||||||
|
<Text style={s.copy}>{copied ? 'Copied' : 'Copy'}</Text>
|
||||||
|
</Pressable>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<Text style={s.step}>{code ? '2.' : '1.'} Download the new version</Text>
|
||||||
|
<Pressable style={s.btn} onPress={() => open(apkUrl)}>
|
||||||
|
<Text style={s.btnText}>Download the update</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Text style={s.step}>{code ? '3.' : '2.'} Uninstall Goon, then open the file</Text>
|
||||||
|
<Text style={s.note}>
|
||||||
|
Long-press the app icon, Uninstall. Then open the .apk from your notifications or
|
||||||
|
Downloads.
|
||||||
|
</Text>
|
||||||
|
{code ? (
|
||||||
|
<>
|
||||||
|
<Text style={s.step}>4. Paste the code back</Text>
|
||||||
|
<Text style={s.note}>Settings, Backup & sync, Restore: paste the code, tap Import.</Text>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Pressable style={s.btn} onPress={() => open(apkUrl)}>
|
||||||
|
<Text style={s.btnText}>Download the update</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Text style={s.note}>
|
||||||
|
Open the downloaded .apk from your notifications or Downloads and tap Install. It
|
||||||
|
installs over this version, your favourites and settings stay.
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Pressable onPress={() => open(SITE_URL)} hitSlop={8}>
|
||||||
|
<Text style={s.link}>Download does not start? Open goon-foss.org</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Text style={s.footer}>Installed: runtime {runtime || '?'}</Text>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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' },
|
||||||
|
});
|
||||||
7
mobile/update-required.tsx
Normal file
7
mobile/update-required.tsx
Normal file
|
|
@ -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);
|
||||||
|
|
@ -141,15 +141,39 @@ def main() -> int:
|
||||||
ap = argparse.ArgumentParser()
|
ap = argparse.ArgumentParser()
|
||||||
ap.add_argument("--runtime", default=RUNTIME_DEFAULT)
|
ap.add_argument("--runtime", default=RUNTIME_DEFAULT)
|
||||||
ap.add_argument("--skip-export", action="store_true", help="reuse existing dist/")
|
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()
|
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 not args.skip_export:
|
||||||
if DIST.exists():
|
if DIST.exists():
|
||||||
import shutil
|
import shutil
|
||||||
shutil.rmtree(DIST)
|
shutil.rmtree(DIST)
|
||||||
# `--source-maps` dokłada `.hbc.map`, potrzebną do odczytania śladów JS
|
# `--source-maps` dokłada `.hbc.map`, potrzebną do odczytania śladów JS
|
||||||
# w Sentry. Mapa NIE jedzie na serwer, kasuje ją `strip_sourcemaps()`.
|
# 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()
|
upload_sourcemaps()
|
||||||
strip_sourcemaps()
|
strip_sourcemaps()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue