Klucz podpisujacy stare buildy przepadl, wiec update in-place jest niemozliwy: Android nadpisze aplikacje tylko buildem z tym samym podpisem. Odinstalowanie czysci SecureStore, a w nim goon.device_id, czyli kod synchronizacji, bez ktorego ulubione/postep zostaja na serwerze bez sposobu wskazania ich. Baner pokazuje kod na wierzchu i prowadzi przez kroki (kopiuj kod, pobierz, odinstaluj, wklej kod). expo-keep-awake: sprawdzenie stringow w dexach APK 0.2.1 potwierdzilo, ze modulu ExpoKeepAwake tam nie ma (jest tylko interfejs z expo-modules-core), wiec ekranu gasnacego w trakcie ogladania NIE dalo sie naprawic przez OTA. Teraz jawna zaleznosc, autolinking ja widzi, wchodzi do APK 0.2.2 (runtime 1.2). UWAGA przy publikacji banera na runtime 1.1: manifest bierze runtimeVersion z argumentu --runtime, wiec 1.1 jest serwowane poprawnie mimo app.json=1.2, ale extras nadal niosa app.json (APP_VERSION zaczyna raportowac 0.2.2). Dlatego serwerowe /version trzeba bumpnac do 0.2.2 razem z wgraniem APK, inaczej stary dialog Update available wyskoczy z mylacym Server 0.2.1 / App 0.2.2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
226 lines
8 KiB
TypeScript
226 lines
8 KiB
TypeScript
/**
|
||
* 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 (
|
||
<>
|
||
<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}>
|
||
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.
|
||
</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 (notes, a message to yourself). 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 downloaded file</Text>
|
||
<Text style={styles.note}>
|
||
Long-press the app icon, Uninstall. Then open the .apk from your notifications
|
||
or Downloads. If Android asks, allow installing from your browser.
|
||
</Text>
|
||
|
||
<Text style={styles.step}>4. Paste the code back</Text>
|
||
<Text style={styles.note}>
|
||
Settings, Backup & sync, Restore from another device: paste the code, tap
|
||
Import. Everything comes back.
|
||
</Text>
|
||
|
||
<Text style={styles.sorry}>
|
||
Sorry for the hassle. Signing keys cannot be recovered, so this is a one-off:
|
||
updates after this one go back to installing themselves.
|
||
</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' },
|
||
});
|