feat(mobile): baner migracji na nowo podpisane APK + expo-keep-awake do buildu
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>
This commit is contained in:
parent
72b7b561e5
commit
0c818ac1df
10 changed files with 307 additions and 24 deletions
|
|
@ -93,8 +93,8 @@ android {
|
||||||
applicationId 'com.goon.mobile'
|
applicationId 'com.goon.mobile'
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 11
|
versionCode 12
|
||||||
versionName "0.2.1"
|
versionName "0.2.2"
|
||||||
}
|
}
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
debug {
|
debug {
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "goon",
|
"name": "goon",
|
||||||
"slug": "goon",
|
"slug": "goon",
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "automatic",
|
||||||
"newArchEnabled": false,
|
"newArchEnabled": false,
|
||||||
"runtimeVersion": "1.1",
|
"runtimeVersion": "1.2",
|
||||||
"updates": {
|
"updates": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"url": "https://api.goon-foss.org/expo-updates/manifest",
|
"url": "https://api.goon-foss.org/expo-updates/manifest",
|
||||||
|
|
|
||||||
21
mobile/package-lock.json
generated
21
mobile/package-lock.json
generated
|
|
@ -20,6 +20,7 @@
|
||||||
"expo-haptics": "~14.0.1",
|
"expo-haptics": "~14.0.1",
|
||||||
"expo-image": "~2.0.7",
|
"expo-image": "~2.0.7",
|
||||||
"expo-intent-launcher": "~12.0.2",
|
"expo-intent-launcher": "~12.0.2",
|
||||||
|
"expo-keep-awake": "^14.0.3",
|
||||||
"expo-linear-gradient": "~14.0.2",
|
"expo-linear-gradient": "~14.0.2",
|
||||||
"expo-local-authentication": "~15.0.2",
|
"expo-local-authentication": "~15.0.2",
|
||||||
"expo-screen-capture": "~7.0.1",
|
"expo-screen-capture": "~7.0.1",
|
||||||
|
|
@ -6301,6 +6302,16 @@
|
||||||
"integrity": "sha512-xjGfK9dL0B1wLnOqNkX0jM9p48Y0I5xEPzHude28LY67UmamUyAACkqhZGaPClyPNfdzczk7Ej6WaRMT3HfXvw==",
|
"integrity": "sha512-xjGfK9dL0B1wLnOqNkX0jM9p48Y0I5xEPzHude28LY67UmamUyAACkqhZGaPClyPNfdzczk7Ej6WaRMT3HfXvw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-keep-awake": {
|
||||||
|
"version": "14.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.0.3.tgz",
|
||||||
|
"integrity": "sha512-6Jh94G6NvTZfuLnm2vwIpKe3GdOiVBuISl7FI8GqN0/9UOg9E0WXXp5cDcfAG8bn80RfgLJS8P7EPUGTZyOvhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-linear-gradient": {
|
"node_modules/expo-linear-gradient": {
|
||||||
"version": "14.0.2",
|
"version": "14.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-14.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-14.0.2.tgz",
|
||||||
|
|
@ -6514,16 +6525,6 @@
|
||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo/node_modules/expo-keep-awake": {
|
|
||||||
"version": "14.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.0.3.tgz",
|
|
||||||
"integrity": "sha512-6Jh94G6NvTZfuLnm2vwIpKe3GdOiVBuISl7FI8GqN0/9UOg9E0WXXp5cDcfAG8bn80RfgLJS8P7EPUGTZyOvhg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"expo": "*",
|
|
||||||
"react": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/exponential-backoff": {
|
"node_modules/exponential-backoff": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
"expo-haptics": "~14.0.1",
|
"expo-haptics": "~14.0.1",
|
||||||
"expo-image": "~2.0.7",
|
"expo-image": "~2.0.7",
|
||||||
"expo-intent-launcher": "~12.0.2",
|
"expo-intent-launcher": "~12.0.2",
|
||||||
|
"expo-keep-awake": "^14.0.3",
|
||||||
"expo-linear-gradient": "~14.0.2",
|
"expo-linear-gradient": "~14.0.2",
|
||||||
"expo-local-authentication": "~15.0.2",
|
"expo-local-authentication": "~15.0.2",
|
||||||
"expo-screen-capture": "~7.0.1",
|
"expo-screen-capture": "~7.0.1",
|
||||||
|
|
|
||||||
226
mobile/src/components/MigrationBanner.tsx
Normal file
226
mobile/src/components/MigrationBanner.tsx
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
/**
|
||||||
|
* 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' },
|
||||||
|
});
|
||||||
|
|
@ -17,4 +17,4 @@ import Constants from 'expo-constants';
|
||||||
* też nie idzie do góry, więc consistency jest zachowana.
|
* też nie idzie do góry, więc consistency jest zachowana.
|
||||||
*/
|
*/
|
||||||
export const APP_VERSION: string =
|
export const APP_VERSION: string =
|
||||||
(Constants.expoConfig?.version as string | undefined) || '0.1.9';
|
(Constants.expoConfig?.version as string | undefined) || '0.2.2';
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,17 @@
|
||||||
* `c40fb79f`: "ekran gaśnie w trakcie oglądania" — Android wygasza ekran po timeoucie,
|
* `c40fb79f`: "ekran gaśnie w trakcie oglądania" — Android wygasza ekran po timeoucie,
|
||||||
* bo dotyk nie pada, a samo granie wideo nie trzyma wake-locka).
|
* bo dotyk nie pada, a samo granie wideo nie trzyma wake-locka).
|
||||||
*
|
*
|
||||||
* Dlaczego nie `expo-keep-awake` z importu: pakiet NIE jest w top-level `node_modules`
|
* Historia, żeby nikt nie powtórzył błędu: pierwsza wersja zakładała, że natywny moduł
|
||||||
* (siedzi w `node_modules/expo/node_modules/`), więc metro nie rozwiąże go z naszego
|
* `ExpoKeepAwake` jest już w APK, bo `expo-keep-awake` leży w `node_modules/expo/
|
||||||
* kodu, a dokładanie zależności do package.json nie przeszłoby przez OTA gdyby wersja
|
* node_modules/`. Sprawdzenie stringów w dexach APK 0.2.1 pokazało coś innego — jest tam
|
||||||
* rozjechała się z APK. Natywny moduł `ExpoKeepAwake` (activate/deactivate po tagu) JEST
|
* tylko interfejs `expo.modules.core.interfaces.services.KeepAwakeManager` z
|
||||||
* jednak w buildzie, bo `expo` bundluje go i autolinkuje — więc sięgamy po niego wprost
|
* expo-modules-core, samego modułu nie ma, bo zagnieżdżona kopia nie została
|
||||||
* przez `requireNativeModule` z `expo-modules-core` (ten jest top-level). Dzięki temu
|
* zautolinkowana. Czyli przez OTA tego się NIE dało naprawić. Dlatego `expo-keep-awake`
|
||||||
* całość leci OTA, bez nowego APK.
|
* jest teraz jawną zależnością w package.json i wchodzi do APK 0.2.2 (runtime 1.2).
|
||||||
*
|
*
|
||||||
* Wszystko best-effort: gdyby modułu zabrakło (inny build), no-op zamiast wywalenia
|
* Sięgamy po moduł przez `requireNativeModule`, a nie zwykłym importem: dzięki temu
|
||||||
* playera.
|
* bundle OTA nie jest związany z wersją paczki JS i ten sam kod działa na buildach,
|
||||||
|
* które modułu nie mają. Wszystko best-effort — brak modułu to no-op, nie wywalony player.
|
||||||
*/
|
*/
|
||||||
import { requireNativeModule } from 'expo-modules-core';
|
import { requireNativeModule } from 'expo-modules-core';
|
||||||
|
|
||||||
|
|
|
||||||
23
mobile/src/migrationNotice.ts
Normal file
23
mobile/src/migrationNotice.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
/**
|
||||||
|
* Konfiguracja banera migracyjnego (MigrationBanner).
|
||||||
|
*
|
||||||
|
* Kontekst: klucz podpisujący release APK (v0.2.1 i wcześniejsze) został utracony.
|
||||||
|
* Androidowy update in-place wymaga TEGO SAMEGO podpisu, więc kolejnej wersji nie da
|
||||||
|
* się już zainstalować na wierzchu — trzeba odinstalować starą i zainstalować nową.
|
||||||
|
* Odinstalowanie kasuje SecureStore, a w nim `goon.device_id`, czyli kod synchronizacji.
|
||||||
|
* Bez skopiowanego kodu ulubione, historia i postęp oglądania stają się nieosiągalne
|
||||||
|
* (dane leżą na serwerze pod tym id, ale nie ma jak ich wskazać).
|
||||||
|
*
|
||||||
|
* Stąd baner: pokazuje kod PRZED odinstalowaniem i prowadzi przez kolejne kroki.
|
||||||
|
*
|
||||||
|
* Trafia tylko do APK na runtime 1.1. Nowe APK ma runtime 1.2, czyli inny kanał OTA,
|
||||||
|
* więc nigdy tego banera nie zobaczy — nie trzeba go potem wyłączać zdalnie.
|
||||||
|
*/
|
||||||
|
export const MIGRATION_NOTICE = {
|
||||||
|
/** Master switch — false chowa baner całkowicie (publikacja OTA na runtime 1.1). */
|
||||||
|
enabled: true,
|
||||||
|
/** Wersja do zainstalowania. Pokazywana w banerze. */
|
||||||
|
version: '0.2.2',
|
||||||
|
/** Strona pobierania. Celowo strona, nie plik: link przeżyje zmianę nazwy APK. */
|
||||||
|
downloadUrl: 'https://goon-foss.org/',
|
||||||
|
} as const;
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Image } from 'expo-image';
|
import { Image } from 'expo-image';
|
||||||
import * as Haptics from 'expo-haptics';
|
import * as Haptics from 'expo-haptics';
|
||||||
|
import { MigrationBanner } from '../components/MigrationBanner';
|
||||||
import { SceneTile, sceneGridProps } from '../components/SceneTile';
|
import { SceneTile, sceneGridProps } from '../components/SceneTile';
|
||||||
import { SceneGridSkeleton } from '../components/SceneGridSkeleton';
|
import { SceneGridSkeleton } from '../components/SceneGridSkeleton';
|
||||||
import { Thumb } from '../components/Thumb';
|
import { Thumb } from '../components/Thumb';
|
||||||
|
|
@ -215,6 +216,10 @@ export function ScenesScreen() {
|
||||||
|
|
||||||
{error instanceof Error && <Text style={styles.error}>{error.message}</Text>}
|
{error instanceof Error && <Text style={styles.error}>{error.message}</Text>}
|
||||||
|
|
||||||
|
{/* Nad listą, nie w ListHeaderComponent: tam znikałby przy wyszukiwaniu i odjeżdżał
|
||||||
|
przy scrollu, a to komunikat, którego przegapienie kosztuje usera ulubione. */}
|
||||||
|
<MigrationBanner />
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<SceneGridSkeleton count={8} />
|
<SceneGridSkeleton count={8} />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,32 @@ export async function setAutoRotate(on: boolean): Promise<void> {
|
||||||
await SecureStore.setItemAsync(AUTO_ROTATE_KEY, on ? '1' : '0');
|
await SecureStore.setItemAsync(AUTO_ROTATE_KEY, on ? '1' : '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Baner migracji na nowy podpis APK (patrz `migrationNotice.ts`). Trzymamy moment
|
||||||
|
// odłożenia „na później", nie zwykłą flagę widziano/nie-widziano: to komunikat, który
|
||||||
|
// MUSI wrócić, bo bez przeniesienia kodu synchronizacji user traci ulubione. Wraca po
|
||||||
|
// dobie, chyba że wybierze „Nie pokazuj więcej".
|
||||||
|
const MIGRATION_SNOOZE_KEY = 'goon.migration_snoozed_at';
|
||||||
|
const MIGRATION_DONE_KEY = 'goon.migration_dismissed';
|
||||||
|
const MIGRATION_SNOOZE_MS = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
export async function isMigrationNoticeHidden(): Promise<boolean> {
|
||||||
|
if ((await SecureStore.getItemAsync(MIGRATION_DONE_KEY)) === '1') return true;
|
||||||
|
const at = await SecureStore.getItemAsync(MIGRATION_SNOOZE_KEY);
|
||||||
|
if (!at) return false;
|
||||||
|
const ts = Number(at);
|
||||||
|
// Zegar cofnięty ręcznie / śmieć w store → traktuj jak brak odłożenia (pokaż baner).
|
||||||
|
if (!Number.isFinite(ts)) return false;
|
||||||
|
return Date.now() - ts < MIGRATION_SNOOZE_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function snoozeMigrationNotice(): Promise<void> {
|
||||||
|
await SecureStore.setItemAsync(MIGRATION_SNOOZE_KEY, String(Date.now()));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dismissMigrationNotice(): Promise<void> {
|
||||||
|
await SecureStore.setItemAsync(MIGRATION_DONE_KEY, '1');
|
||||||
|
}
|
||||||
|
|
||||||
export interface Credentials {
|
export interface Credentials {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue