/** * Baner jednorazowej migracji na nowo podpisane APK. * * Dlaczego w ogóle: klucz podpisujący stare buildy przepadł, a Android pozwala nadpisać * aplikację tylko buildem z tym samym podpisem. Nowa wersja wymaga więc odinstalowania * starej, co czyści SecureStore razem z `goon.device_id`, czyli kodem synchronizacji. * Dane (ulubione, zapisane wyszukiwania, ukryte, postęp) zostają na serwerze pod tym id, * ale bez kodu nie ma jak ich odzyskać. Dlatego kod jest w banerze na wierzchu, a nie * schowany w ustawieniach: liczy się, żeby user skopiował go PRZED odinstalowaniem. * * Zwinięty pasek siedzi nad listą scen (nie w ListHeaderComponent, tam znikałby przy * wyszukiwaniu i odjeżdżał przy scrollu). Rozwija się w modal z krokami. */ import React from 'react'; import { Linking, Modal, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'; import * as Clipboard from 'expo-clipboard'; import { MIGRATION_NOTICE, needsManualMigration } from '../migrationNotice'; import { dismissMigrationNotice, getDeviceId, isMigrationNoticeHidden, snoozeMigrationNotice, } from '../storage'; import { theme } from '../theme'; export function MigrationBanner() { const [show, setShow] = React.useState(false); const [open, setOpen] = React.useState(false); const [deviceId, setDeviceId] = React.useState(''); const [copied, setCopied] = React.useState(false); React.useEffect(() => { // Warunek na runtime, nie tylko na kanał OTA: ten sam bundle bywa publikowany na 1.2, // a tam baner byłby szkodliwy (kazałby migrować z wersji, na którą user właśnie przeszedł). if (!needsManualMigration()) return; let cancelled = false; (async () => { try { const [hidden, id] = await Promise.all([isMigrationNoticeHidden(), getDeviceId()]); if (cancelled || hidden) return; setDeviceId(id); setShow(true); } catch { // brak SecureStore → nie ma czym migrować, nie strasz banerem } })(); return () => { cancelled = true; }; }, []); const copy = React.useCallback(async () => { await Clipboard.setStringAsync(deviceId); setCopied(true); }, [deviceId]); const later = React.useCallback(() => { setOpen(false); setShow(false); snoozeMigrationNotice().catch(() => {}); }, []); const done = React.useCallback(() => { setOpen(false); setShow(false); dismissMigrationNotice().catch(() => {}); }, []); if (!show) return null; return ( <> setOpen(true)}> Update {MIGRATION_NOTICE.version} needs a manual install Copy your sync code first, or your favourites do not come across. Tap for the steps. › Update {MIGRATION_NOTICE.version} The new build is signed with a different key, so Android will not install it over the current app. Uninstall first, install the download second. That wipes what is stored on the phone, so copy your sync code before you start. 1. Copy your sync code {deviceId || '…'} {copied ? 'Copied' : 'Copy'} Paste it somewhere safe. Without it your favourites, saved searches, hidden list and watch progress cannot be found again. 2. Download the new version Linking.openURL(MIGRATION_NOTICE.downloadUrl).catch(() => {})} > Open {MIGRATION_NOTICE.downloadUrl.replace(/^https?:\/\//, '').replace(/\/$/, '')} 3. Uninstall Goon, then open the file Long-press the app icon, Uninstall. Then open the .apk from your notifications or Downloads. 4. Paste the code back Settings, Backup & sync, Restore: paste the code, tap Import. A one-off, sorry. Signing keys cannot be recovered, so this once it has to be done by hand. Later updates install themselves again. Done, do not show again Remind me tomorrow ); } const styles = StyleSheet.create({ bar: { flexDirection: 'row', alignItems: 'center', gap: 10, marginHorizontal: 12, marginBottom: 10, paddingVertical: 10, paddingHorizontal: 12, borderRadius: 12, borderWidth: 1, borderColor: theme.accentText, backgroundColor: theme.bgElevated, }, barTitle: { color: theme.fg, fontSize: 14, fontWeight: '700' }, barHint: { color: theme.muted, fontSize: 12, marginTop: 2, lineHeight: 16 }, barChevron: { color: theme.accentText, fontSize: 22, fontWeight: '700' }, backdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.7)', alignItems: 'center', justifyContent: 'center', padding: 24, }, card: { width: '100%', maxHeight: '85%', backgroundColor: theme.bgElevated, borderRadius: 16, borderColor: theme.border, borderWidth: 1, padding: 20, }, title: { color: theme.fg, fontSize: 20, fontWeight: '800', marginBottom: 12 }, scroll: { flexGrow: 0 }, lead: { color: theme.fg, fontSize: 14, lineHeight: 20, marginBottom: 16 }, step: { color: theme.accentText, fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.5, marginTop: 8, marginBottom: 8, }, codeRow: { flexDirection: 'row', alignItems: 'center', gap: 10, backgroundColor: theme.card, borderRadius: 10, borderWidth: 1, borderColor: theme.border, paddingVertical: 10, paddingHorizontal: 12, }, code: { color: theme.fg, fontSize: 13, flex: 1 }, copyBtn: { color: theme.accentText, fontSize: 13, fontWeight: '800' }, note: { color: theme.muted, fontSize: 13, lineHeight: 19, marginTop: 8 }, dlBtn: { backgroundColor: theme.card, borderRadius: 10, borderWidth: 1, borderColor: theme.accentText, paddingVertical: 12, alignItems: 'center', }, dlText: { color: theme.fg, fontSize: 14, fontWeight: '700' }, sorry: { color: theme.mutedDim, fontSize: 12, lineHeight: 18, marginTop: 18 }, primaryBtn: { marginTop: 14, backgroundColor: theme.accent, borderRadius: 10, paddingVertical: 12, alignItems: 'center', }, primaryText: { color: theme.fg, fontSize: 15, fontWeight: '700' }, secondaryBtn: { marginTop: 8, paddingVertical: 10, alignItems: 'center' }, secondaryText: { color: theme.muted, fontSize: 14, fontWeight: '600' }, });