goon/mobile/src/components/MigrationBanner.tsx
goon-foss a4486980a4 style: myslniki em/en z komentarzy i docstringow (calosc repo)
1745 wystapien w 291 plikach (backend, scripts, mobile). Dominowal jeden ksztalt,
' — ' laczace zdania, wiec poszedl przecinek; myslnik otwierajacy mysl zdjety;
zakresy 0–5 i A–Z na dywiz.

Podzial komentarz/kod robil skaner stanow, nie regex, zeby nie ruszyc myslnika
stojacego w stringu albo w wyrazeniu regularnym. Potwierdzone maszynowo: po wycieciu
komentarzy z wersji HEAD i z dysku obie sa identyczne we wszystkich 291 plikach,
czyli poza komentarzami nie zmienilo sie nic. 233 pliki .py parsuja sie bez bledu,
tsc ma te same 11 bledow w tych samych 3 plikach co przed zmiana.

Dwie rzeczy wymagaly poprawki po automacie: wyrownane listy definicji, gdzie myslnik
byl separatorem kolumny (37 miejsc, kolumna przywrocona), i trzy przeniesienia mysli
do nowej linii, gdzie zostawal wiszacy przecinek na poczatku (poprawione recznie).

Bundla nie republikuje: Hermes i tak nie niesie komentarzy, wiec dla uzytkownika
to zero zmiany.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:51:57 +02:00

226 lines
8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 (
<>
<Pressable style={styles.bar} onPress={() => setOpen(true)}>
<View style={{ flex: 1 }}>
<Text style={styles.barTitle}>Update {MIGRATION_NOTICE.version} needs a manual install</Text>
<Text style={styles.barHint}>
Copy your sync code first, or your favourites do not come across. Tap for the steps.
</Text>
</View>
<Text style={styles.barChevron}>›</Text>
</Pressable>
<Modal visible={open} transparent animationType="fade" onRequestClose={later}>
<View style={styles.backdrop}>
<View style={styles.card}>
<Text style={styles.title}>Update {MIGRATION_NOTICE.version}</Text>
<ScrollView style={styles.scroll} contentContainerStyle={{ paddingBottom: 4 }}>
<Text style={styles.lead}>
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.
</Text>
<Text style={styles.step}>1. Copy your sync code</Text>
<Pressable style={styles.codeRow} onPress={copy}>
<Text style={styles.code} numberOfLines={1}>
{deviceId || '…'}
</Text>
<Text style={styles.copyBtn}>{copied ? 'Copied' : 'Copy'}</Text>
</Pressable>
<Text style={styles.note}>
Paste it somewhere safe. Without it your favourites, saved searches, hidden
list and watch progress cannot be found again.
</Text>
<Text style={styles.step}>2. Download the new version</Text>
<Pressable
style={styles.dlBtn}
onPress={() => Linking.openURL(MIGRATION_NOTICE.downloadUrl).catch(() => {})}
>
<Text style={styles.dlText}>Open {MIGRATION_NOTICE.downloadUrl.replace(/^https?:\/\//, '').replace(/\/$/, '')}</Text>
</Pressable>
<Text style={styles.step}>3. Uninstall Goon, then open the file</Text>
<Text style={styles.note}>
Long-press the app icon, Uninstall. Then open the .apk from your notifications
or Downloads.
</Text>
<Text style={styles.step}>4. Paste the code back</Text>
<Text style={styles.note}>
Settings, Backup &amp; sync, Restore: paste the code, tap Import.
</Text>
<Text style={styles.sorry}>
A one-off, sorry. Signing keys cannot be recovered, so this once it has to be
done by hand. Later updates install themselves again.
</Text>
</ScrollView>
<Pressable style={styles.primaryBtn} onPress={done}>
<Text style={styles.primaryText}>Done, do not show again</Text>
</Pressable>
<Pressable style={styles.secondaryBtn} onPress={later}>
<Text style={styles.secondaryText}>Remind me tomorrow</Text>
</Pressable>
</View>
</View>
</Modal>
</>
);
}
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' },
});