goon/mobile/src/components/MigrationBanner.tsx
goon-foss 7deda639bf fix(theme): rozdziel role akcentu i dodaj ramke, ktora cokolwiek oddziela
Pomiar WCAG na realnych parach z apki (2026-08-13):

    accent #B23A48 jako tekst:  3,21 na bg | 2,96 na bgElevated | 2,75 na card
    border #3A3128 jako granica: 1,48 na bg | 1,27 na card

Czyli kolor MARKI byl ponizej nawet progu 3:1 dla elementow interfejsu,
a nie tylko ponizej 4,5 dla tekstu. Dotyczylo to aktywnych zakladek,
gwiazdek ulubionych, linkow i chipow. Ramki przy 1,27 nie byly granica,
tylko cieniem.

NIE zmieniam koloru marki. Podniesienie samego `accent` do progu tekstu
wymagaloby zejscia w roz (#CE6975), co likwiduje tozsamosc, ktorej broni
naglowek theme.ts. Zamiast tego rozdzielam ROLE tego samego koloru:

  accent      zostaje do WYPELNIEN — jasny tekst na nim ma 5,03, przechodzi
  accentText  #C75260 dla tekstu i obwodek — 4,30 / 3,96 / 3,68

62 podmiany w 24 plikach (34 color, 26 borderColor, 2 thumbColor).
38 wypelnien nietknietych. Sprawdzone skryptem, ze zaden zmieniony tekst
nie stoi na tle akcentu.

borderStrong #7B7065 dla granic NIOSACYCH ZNACZENIE (pola tekstowe, 12
miejsc); dekoracyjne wloski zostaja na `border`. Wartosc dobrana pod obie
powierzchnie, bo pola leza i na bg, i na card: #6B6157 dawal na karcie
2,66, a #73695E dokladnie 2,999, czyli wciaz ponizej progu.
2026-08-14 09:50:08 +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.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' },
});