diff --git a/App.tsx b/App.tsx index da0ca8d2..e59370ab 100644 --- a/App.tsx +++ b/App.tsx @@ -29,6 +29,8 @@ import { ThemeProvider, useTheme } from './src/contexts/ThemeContext'; import { TrailerProvider } from './src/contexts/TrailerContext'; import SplashScreen from './src/components/SplashScreen'; import UpdatePopup from './src/components/UpdatePopup'; +import MajorUpdateOverlay from './src/components/MajorUpdateOverlay'; +import { useGithubMajorUpdate } from './src/hooks/useGithubMajorUpdate'; import { useUpdatePopup } from './src/hooks/useUpdatePopup'; import AsyncStorage from '@react-native-async-storage/async-storage'; import * as Sentry from '@sentry/react-native'; @@ -84,6 +86,9 @@ const ThemedApp = () => { handleUpdateLater, handleDismiss, } = useUpdatePopup(); + + // GitHub major/minor release overlay + const githubUpdate = useGithubMajorUpdate(); // Check onboarding status and initialize services useEffect(() => { @@ -165,6 +170,14 @@ const ThemedApp = () => { isInstalling={isInstalling} /> )} + diff --git a/src/components/MajorUpdateOverlay.tsx b/src/components/MajorUpdateOverlay.tsx new file mode 100644 index 00000000..c595e24c --- /dev/null +++ b/src/components/MajorUpdateOverlay.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { Modal, View, Text, StyleSheet, TouchableOpacity, Linking } from 'react-native'; +import { MaterialIcons } from '@expo/vector-icons'; +import { useTheme } from '../contexts/ThemeContext'; + +interface Props { + visible: boolean; + latestTag?: string; + releaseNotes?: string; + releaseUrl?: string; + onDismiss: () => void; + onLater: () => void; +} + +const MajorUpdateOverlay: React.FC = ({ visible, latestTag, releaseNotes, releaseUrl, onDismiss, onLater }) => { + const { currentTheme } = useTheme(); + + if (!visible) return null; + + return ( + + + + + + + + Major update available + {!!latestTag && ( + Latest: {latestTag} + )} + + + {!!releaseNotes && ( + + + {releaseNotes} + + + )} + + + {releaseUrl ? ( + Linking.openURL(releaseUrl)}> + + View release + + ) : null} + + + + Later + + + Dismiss + + + + + + + ); +}; + +const styles = StyleSheet.create({ + backdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.8)', alignItems: 'center', justifyContent: 'center', padding: 20 }, + card: { width: 380, maxWidth: '100%', borderRadius: 20, borderWidth: 1, overflow: 'hidden' }, + header: { alignItems: 'center', paddingTop: 28, paddingBottom: 16, paddingHorizontal: 20 }, + iconCircle: { width: 56, height: 56, borderRadius: 28, alignItems: 'center', justifyContent: 'center', marginBottom: 12 }, + title: { fontSize: 20, fontWeight: '700', marginBottom: 6 }, + version: { fontSize: 14 }, + notesBox: { marginHorizontal: 20, marginBottom: 16, padding: 12, borderRadius: 12, backgroundColor: 'rgba(255,255,255,0.08)' }, + notes: { fontSize: 14, lineHeight: 20 }, + actions: { paddingHorizontal: 20, paddingBottom: 20 }, + primaryBtn: { flexDirection: 'row', alignItems: 'center', gap: 8, justifyContent: 'center', paddingVertical: 12, borderRadius: 12, marginBottom: 12 }, + primaryText: { color: '#fff', fontSize: 16, fontWeight: '600' }, + secondaryRow: { flexDirection: 'row', gap: 10 }, + secondaryBtn: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingVertical: 12, borderRadius: 12, borderWidth: 1 }, + secondaryText: { fontSize: 15, fontWeight: '500' }, +}); + +export default MajorUpdateOverlay; + + diff --git a/src/hooks/useGithubMajorUpdate.ts b/src/hooks/useGithubMajorUpdate.ts new file mode 100644 index 00000000..1e311c6b --- /dev/null +++ b/src/hooks/useGithubMajorUpdate.ts @@ -0,0 +1,65 @@ +import { useCallback, useEffect, useState } from 'react'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import * as Updates from 'expo-updates'; +import { getDisplayedAppVersion } from '../utils/version'; +import { fetchLatestGithubRelease, isAnyUpgrade } from '../services/githubReleaseService'; + +const DISMISSED_KEY = '@github_major_update_dismissed_version'; + +export interface MajorUpdateData { + visible: boolean; + latestTag?: string; + releaseNotes?: string; + releaseUrl?: string; + onDismiss: () => void; + onLater: () => void; + refresh: () => void; +} + +export function useGithubMajorUpdate(): MajorUpdateData { + const [visible, setVisible] = useState(false); + const [latestTag, setLatestTag] = useState(); + const [releaseNotes, setReleaseNotes] = useState(); + const [releaseUrl, setReleaseUrl] = useState(); + + const check = useCallback(async () => { + try { + // Always compare with Settings screen version + const current = getDisplayedAppVersion() || Updates.runtimeVersion || '0.0.0'; + const info = await fetchLatestGithubRelease(); + if (!info?.tag_name) return; + + const dismissed = await AsyncStorage.getItem(DISMISSED_KEY); + if (dismissed === info.tag_name) return; + + // "Later" is session-only now, no persisted snooze + + const shouldShow = isAnyUpgrade(current, info.tag_name); + if (shouldShow) { + setLatestTag(info.tag_name); + setReleaseNotes(info.body); + setReleaseUrl(info.html_url); + setVisible(true); + } + } catch { + // ignore + } + }, []); + + useEffect(() => { + check(); + }, [check]); + + const onDismiss = useCallback(async () => { + if (latestTag) await AsyncStorage.setItem(DISMISSED_KEY, latestTag); + setVisible(false); + }, [latestTag]); + + const onLater = useCallback(async () => { + setVisible(false); + }, []); + + return { visible, latestTag, releaseNotes, releaseUrl, onDismiss, onLater, refresh: check }; +} + + diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index a5455bbc..2089337f 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -30,6 +30,7 @@ import { useAccount } from '../contexts/AccountContext'; import { catalogService } from '../services/catalogService'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import * as Sentry from '@sentry/react-native'; +import { getDisplayedAppVersion } from '../utils/version'; import CustomAlert from '../components/CustomAlert'; import ProfileIcon from '../components/icons/ProfileIcon'; import PluginIcon from '../components/icons/PluginIcon'; @@ -643,7 +644,7 @@ const SettingsScreen: React.FC = () => { /> = 768; @@ -65,6 +69,7 @@ const UpdateScreen: React.FC = () => { const navigation = useNavigation>(); const { currentTheme } = useTheme(); const insets = useSafeAreaInsets(); + const github = useGithubMajorUpdate(); // CustomAlert state const [alertVisible, setAlertVisible] = useState(false); @@ -144,6 +149,8 @@ const UpdateScreen: React.FC = () => { })(); } checkForUpdates(); + // Also refresh GitHub section on mount (works in dev and prod) + try { github.refresh(); } catch {} if (Platform.OS === 'android') { try { toast('Checking for updates…', { duration: 1200, position: ToastPosition.TOP }); @@ -517,6 +524,58 @@ const UpdateScreen: React.FC = () => { {/* Developer Logs removed */} + {/* GitHub Release (compact) – only show when update is available */} + {github.latestTag && isAnyUpgrade(getDisplayedAppVersion(), github.latestTag) ? ( + + + + + + + Current: + + {getDisplayedAppVersion()} + + + + + + + + Latest: + + {github.latestTag} + + + + {github.releaseNotes ? ( + + Notes: + + {github.releaseNotes} + + + ) : null} + + + + github.releaseUrl ? Linking.openURL(github.releaseUrl as string) : null} + activeOpacity={0.8} + > + + View Release + + + + + + ) : null} + {false && ( diff --git a/src/services/githubReleaseService.ts b/src/services/githubReleaseService.ts new file mode 100644 index 00000000..48471898 --- /dev/null +++ b/src/services/githubReleaseService.ts @@ -0,0 +1,62 @@ +import { Platform } from 'react-native'; + +export interface GithubReleaseInfo { + tag_name: string; + name?: string; + body?: string; + html_url?: string; + published_at?: string; +} + +const GITHUB_LATEST_RELEASE_URL = 'https://api.github.com/repos/tapframe/NuvioStreaming/releases/latest'; + +export async function fetchLatestGithubRelease(): Promise { + try { + const res = await fetch(GITHUB_LATEST_RELEASE_URL, { + headers: { + 'Accept': 'application/vnd.github+json', + // Identify app a bit; avoid user agent blocks + 'User-Agent': `Nuvio/${Platform.OS}`, + }, + }); + if (!res.ok) return null; + const json = await res.json(); + return { + tag_name: json.tag_name, + name: json.name, + body: json.body, + html_url: json.html_url, + published_at: json.published_at, + }; + } catch { + return null; + } +} + +export function parseSemver(version: string): [number, number, number] | null { + const m = version.trim().replace(/^v/, '').match(/^(\d+)\.(\d+)\.(\d+)/); + if (!m) return null; + return [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)]; +} + +export function isMajorOrMinorUpgrade(current: string, latest: string): boolean { + const a = parseSemver(current); + const b = parseSemver(latest); + if (!a || !b) return false; + // Major or minor bump when (b.major > a.major) or (same major and b.minor > a.minor) + if (b[0] > a[0]) return true; + if (b[0] === a[0] && b[1] > a[1]) return true; + return false; +} + +// Return true if latest > current for semver including patch +export function isAnyUpgrade(current: string, latest: string): boolean { + const a = parseSemver(current); + const b = parseSemver(latest); + if (!a || !b) return false; + if (b[0] !== a[0]) return b[0] > a[0]; + if (b[1] !== a[1]) return b[1] > a[1]; + return b[2] > a[2]; +} + + diff --git a/src/utils/version.ts b/src/utils/version.ts new file mode 100644 index 00000000..4e8c6df6 --- /dev/null +++ b/src/utils/version.ts @@ -0,0 +1,10 @@ +// Single source of truth for the app version displayed in Settings +// Update this when bumping app version + +export const APP_VERSION = '1.2.1'; + +export function getDisplayedAppVersion(): string { + return APP_VERSION; +} + + diff --git a/update-version.sh b/update-version.sh index 8d278716..899335c4 100644 --- a/update-version.sh +++ b/update-version.sh @@ -1,7 +1,7 @@ #!/bin/bash # Version Update Script for Nuvio App -# Updates version across app.json, SettingsScreen.tsx, and iOS Info.plist +# Updates version across app.json, src/utils/version.ts, and iOS/Android build files set -e # Exit on any error @@ -56,12 +56,12 @@ fi # File paths APP_JSON="./app.json" -SETTINGS_SCREEN="./src/screens/SettingsScreen.tsx" +VERSION_TS="./src/utils/version.ts" INFO_PLIST="./ios/Nuvio/Info.plist" ANDROID_BUILD_GRADLE="./android/app/build.gradle" # Check if files exist -for file in "$APP_JSON" "$SETTINGS_SCREEN" "$INFO_PLIST" "$ANDROID_BUILD_GRADLE"; do +for file in "$APP_JSON" "$VERSION_TS" "$INFO_PLIST" "$ANDROID_BUILD_GRADLE"; do if [ ! -f "$file" ]; then print_error "File not found: $file" exit 1 @@ -88,7 +88,7 @@ print_status "New build number: $NEW_BUILD_NUMBER" # Backup files print_status "Creating backups..." cp "$APP_JSON" "${APP_JSON}.backup" -cp "$SETTINGS_SCREEN" "${SETTINGS_SCREEN}.backup" +cp "$VERSION_TS" "${VERSION_TS}.backup" cp "$INFO_PLIST" "${INFO_PLIST}.backup" cp "$ANDROID_BUILD_GRADLE" "${ANDROID_BUILD_GRADLE}.backup" @@ -96,7 +96,7 @@ cp "$ANDROID_BUILD_GRADLE" "${ANDROID_BUILD_GRADLE}.backup" restore_backups() { print_warning "Restoring backups due to error..." mv "${APP_JSON}.backup" "$APP_JSON" - mv "${SETTINGS_SCREEN}.backup" "$SETTINGS_SCREEN" + mv "${VERSION_TS}.backup" "$VERSION_TS" mv "${INFO_PLIST}.backup" "$INFO_PLIST" mv "${ANDROID_BUILD_GRADLE}.backup" "$ANDROID_BUILD_GRADLE" } @@ -116,11 +116,11 @@ sed -i '' "s/\"versionCode\": [0-9]*/\"versionCode\": $NEW_BUILD_NUMBER/g" "$APP sed -i '' "s/\"buildNumber\": \"[^\"]*\"/\"buildNumber\": \"$NEW_BUILD_NUMBER\"/g" "$APP_JSON" print_success "Updated app.json" -# Update SettingsScreen.tsx -print_status "Updating SettingsScreen.tsx..." -# Note: Use BSD sed compatible regex on macOS. Enable extended regex with -E. -sed -E -i '' "s/description=\"[0-9]+\.[0-9]+\.[0-9]+(-[^\"]*)?\"/description=\"$NEW_VERSION\"/g" "$SETTINGS_SCREEN" -print_success "Updated SettingsScreen.tsx" +# Update src/utils/version.ts +print_status "Updating src/utils/version.ts..." +# Replace the APP_VERSION constant value +sed -E -i '' "s/export const APP_VERSION = '\\S*';/export const APP_VERSION = '$NEW_VERSION';/g" "$VERSION_TS" +print_success "Updated src/utils/version.ts" # Update Info.plist print_status "Updating Info.plist..." @@ -152,11 +152,11 @@ else exit 1 fi -# Check SettingsScreen.tsx -if grep -q "description=\"$NEW_VERSION\"" "$SETTINGS_SCREEN"; then - print_success "SettingsScreen.tsx updated correctly" +# Check src/utils/version.ts +if grep -q "export const APP_VERSION = '$NEW_VERSION';" "$VERSION_TS"; then + print_success "src/utils/version.ts updated correctly" else - print_error "SettingsScreen.tsx update verification failed" + print_error "src/utils/version.ts update verification failed" exit 1 fi @@ -180,14 +180,14 @@ fi # Clean up backups print_status "Cleaning up backups..." -rm "${APP_JSON}.backup" "${SETTINGS_SCREEN}.backup" "${INFO_PLIST}.backup" "${ANDROID_BUILD_GRADLE}.backup" +rm "${APP_JSON}.backup" "${VERSION_TS}.backup" "${INFO_PLIST}.backup" "${ANDROID_BUILD_GRADLE}.backup" print_success "Version update completed successfully!" print_status "Summary:" echo " Version: $NEW_VERSION" echo " Runtime Version: $NEW_VERSION" echo " Build Number: $NEW_BUILD_NUMBER" -echo " Files updated: app.json, SettingsScreen.tsx, Info.plist, Android build.gradle" +echo " Files updated: app.json, src/utils/version.ts, Info.plist, Android build.gradle" echo "" print_status "Next steps:" echo " 1. Test the app to ensure everything works correctly"