/**
* 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 } 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(() => {
if (!MIGRATION_NOTICE.enabled) 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}
This one has to be installed by hand: the new build is signed with a different
key, so Android will not put it on top of the current app. You uninstall Goon
first, then install the download. That wipes what is stored on the phone, so
copy your sync code now and paste it back afterwards.
1. Copy your sync code
{deviceId || '…'}
{copied ? 'Copied' : 'Copy'}
Paste it somewhere safe (notes, a message to yourself). 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 downloaded file
Long-press the app icon, Uninstall. Then open the .apk from your notifications
or Downloads. If Android asks, allow installing from your browser.
4. Paste the code back
Settings, Backup & sync, Restore from another device: paste the code, tap
Import. Everything comes back.
Sorry for the hassle. Signing keys cannot be recovered, so this is a one-off:
updates after this one go back to installing themselves.
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.accent,
backgroundColor: theme.bgElevated,
},
barTitle: { color: theme.fg, fontSize: 14, fontWeight: '700' },
barHint: { color: theme.muted, fontSize: 12, marginTop: 2, lineHeight: 16 },
barChevron: { color: theme.accent, 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.accent,
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.accent, fontSize: 13, fontWeight: '800' },
note: { color: theme.muted, fontSize: 13, lineHeight: 19, marginTop: 8 },
dlBtn: {
backgroundColor: theme.card,
borderRadius: 10,
borderWidth: 1,
borderColor: theme.accent,
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' },
});