From 031c0c877240b343c842bcccaec1c88943634afa Mon Sep 17 00:00:00 2001 From: tapframe Date: Fri, 9 Jan 2026 01:11:29 +0530 Subject: [PATCH] added dev options to prod builds --- src/screens/SettingsScreen.tsx | 26 +- src/screens/settings/AboutSettingsScreen.tsx | 291 +++++++++++++++++- .../settings/DeveloperSettingsScreen.tsx | 20 +- 3 files changed, 326 insertions(+), 11 deletions(-) diff --git a/src/screens/SettingsScreen.tsx b/src/screens/SettingsScreen.tsx index 388687e..6cebc70 100644 --- a/src/screens/SettingsScreen.tsx +++ b/src/screens/SettingsScreen.tsx @@ -205,12 +205,26 @@ const SettingsScreen: React.FC = () => { // States for dynamic content const [mdblistKeySet, setMdblistKeySet] = useState(false); + const [developerModeEnabled, setDeveloperModeEnabled] = useState(false); const [totalDownloads, setTotalDownloads] = useState(0); const [displayDownloads, setDisplayDownloads] = useState(null); // Use Realtime Config Hook const settingsConfig = useRealtimeConfig(); + // Load developer mode state + useEffect(() => { + const loadDevModeState = async () => { + try { + const devModeEnabled = await mmkvStorage.getItem('developer_mode_enabled'); + setDeveloperModeEnabled(devModeEnabled === 'true'); + } catch (error) { + if (__DEV__) console.error('Failed to load developer mode state:', error); + } + }; + loadDevModeState(); + }, []); + // Scroll to top ref and handler const mobileScrollViewRef = useRef(null); const tabletScrollViewRef = useRef(null); @@ -236,6 +250,10 @@ const SettingsScreen: React.FC = () => { const mdblistKey = await mmkvStorage.getItem('mdblist_api_key'); setMdblistKeySet(!!mdblistKey); + // Check developer mode status + const devModeEnabled = await mmkvStorage.getItem('developer_mode_enabled'); + setDeveloperModeEnabled(devModeEnabled === 'true'); + // Load GitHub total downloads const downloads = await fetchTotalDownloads(); if (downloads !== null) { @@ -339,7 +357,7 @@ const SettingsScreen: React.FC = () => { // Filter categories based on conditions const visibleCategories = SETTINGS_CATEGORIES.filter(category => { if (settingsConfig?.categories?.[category.id]?.visible === false) return false; - if (category.id === 'developer' && !__DEV__) return false; + if (category.id === 'developer' && !__DEV__ && !developerModeEnabled) return false; if (category.id === 'cache' && !mdblistKeySet) return false; return true; }); @@ -380,7 +398,7 @@ const SettingsScreen: React.FC = () => { return ; case 'developer': - return __DEV__ ? ( + return (__DEV__ || developerModeEnabled) ? ( { /> - {/* Developer - only in DEV mode */} - {__DEV__ && ( + {/* Developer - visible in DEV mode or when developer mode is enabled */} + {(__DEV__ || developerModeEnabled) && ( void; } /** @@ -28,17 +31,60 @@ interface AboutSettingsContentProps { */ export const AboutSettingsContent: React.FC = ({ isTablet = false, - displayDownloads: externalDisplayDownloads + displayDownloads: externalDisplayDownloads, + onDevModeChange }) => { const { t } = useTranslation(); const navigation = useNavigation>(); const { currentTheme } = useTheme(); const [internalDisplayDownloads, setInternalDisplayDownloads] = useState(null); + const [developerModeEnabled, setDeveloperModeEnabled] = useState(false); + const [tapCount, setTapCount] = useState(0); + const tapTimeoutRef = useRef(null); + + // Developer code entry modal state + const [showCodeModal, setShowCodeModal] = useState(false); + const [codeInput, setCodeInput] = useState(''); + + // CustomAlert state + const [alertState, setAlertState] = useState<{ + visible: boolean; + title: string; + message: string; + actions: Array<{ label: string; onPress: () => void; style?: object }>; + }>({ + visible: false, + title: '', + message: '', + actions: [{ label: 'OK', onPress: () => { } }] + }); + + const showAlert = (title: string, message: string, actions?: Array<{ label: string; onPress: () => void; style?: object }>) => { + setAlertState({ + visible: true, + title, + message, + actions: actions || [{ label: 'OK', onPress: () => { } }] + }); + }; // Use external downloads if provided (for tablet inline use), otherwise load internally const displayDownloads = externalDisplayDownloads ?? internalDisplayDownloads; + // Load developer mode state on mount + useEffect(() => { + const loadDevModeState = async () => { + try { + const devModeEnabled = await mmkvStorage.getItem('developer_mode_enabled'); + setDeveloperModeEnabled(devModeEnabled === 'true'); + } catch (error) { + console.error('Failed to load developer mode state:', error); + } + }; + loadDevModeState(); + }, []); + useEffect(() => { // Only load downloads internally if not provided externally if (externalDisplayDownloads === undefined) { @@ -52,6 +98,88 @@ export const AboutSettingsContent: React.FC = ({ } }, [externalDisplayDownloads]); + const handleVersionTap = () => { + // If already in developer mode, do nothing on tap + if (developerModeEnabled) return; + + // Clear previous timeout + if (tapTimeoutRef.current) { + clearTimeout(tapTimeoutRef.current); + } + + const newTapCount = tapCount + 1; + setTapCount(newTapCount); + + // Reset tap count after 2 seconds of no tapping + tapTimeoutRef.current = setTimeout(() => { + setTapCount(0); + }, 2000); + + // Trigger developer mode unlock after 5 taps + if (newTapCount >= 5) { + setTapCount(0); + promptForDeveloperCode(); + } + }; + + const promptForDeveloperCode = () => { + setCodeInput(''); + setShowCodeModal(true); + }; + + const verifyDeveloperCode = async () => { + setShowCodeModal(false); + const expectedCode = process.env.EXPO_PUBLIC_DEV_MODE_CODE || '815787'; + if (codeInput === expectedCode) { + try { + await mmkvStorage.setItem('developer_mode_enabled', 'true'); + setDeveloperModeEnabled(true); + onDevModeChange?.(true); + showAlert( + t('settings.developer_mode.enabled_title', 'Developer Mode Enabled'), + t('settings.developer_mode.enabled_message', 'Developer tools are now available in Settings.') + ); + } catch (error) { + console.error('Failed to save developer mode state:', error); + } + } else { + showAlert( + t('settings.developer_mode.invalid_code_title', 'Invalid Code'), + t('settings.developer_mode.invalid_code_message', 'The code you entered is incorrect.') + ); + } + setCodeInput(''); + }; + + const handleDisableDeveloperMode = () => { + showAlert( + t('settings.developer_mode.disable_title', 'Disable Developer Mode'), + t('settings.developer_mode.disable_message', 'Are you sure you want to disable developer mode?'), + [ + { + label: t('common.cancel', 'Cancel'), + onPress: () => { }, + }, + { + label: t('common.disable', 'Disable'), + onPress: async () => { + try { + await mmkvStorage.setItem('developer_mode_enabled', 'false'); + setDeveloperModeEnabled(false); + onDevModeChange?.(false); + showAlert( + t('settings.developer_mode.disabled_title', 'Developer Mode Disabled'), + t('settings.developer_mode.disabled_message', 'Developer tools are now hidden.') + ); + } catch (error) { + console.error('Failed to save developer mode state:', error); + } + }, + }, + ] + ); + }; + return ( <> @@ -80,6 +208,7 @@ export const AboutSettingsContent: React.FC = ({ title={t('settings.items.version')} description={getDisplayedAppVersion()} icon="info" + onPress={handleVersionTap} isTablet={isTablet} /> = ({ icon="users" renderControl={() => } onPress={() => navigation.navigate('Contributors')} - isLast + isLast={!developerModeEnabled} isTablet={isTablet} /> + {developerModeEnabled && ( + } + isLast + isTablet={isTablet} + /> + )} + + {/* Developer Code Entry Modal */} + setShowCodeModal(false)} + statusBarTranslucent + > + + setShowCodeModal(false)} + /> + + + {t('settings.developer_mode.enter_code_title', 'Enable Developer Mode')} + + + {t('settings.developer_mode.enter_code_message', 'Enter the developer code to enable developer mode:')} + + + + { + setShowCodeModal(false); + setCodeInput(''); + }} + > + + {t('common.cancel', 'Cancel')} + + + + + {t('common.confirm', 'Confirm')} + + + + + + + + {/* Custom Alert */} + setAlertState(prev => ({ ...prev, visible: false }))} + /> ); }; @@ -308,4 +517,78 @@ const styles = StyleSheet.create({ }, }); +// Styles for the developer code entry modal +const modalStyles = StyleSheet.create({ + overlay: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.85)', + }, + backdrop: { + ...StyleSheet.absoluteFillObject, + }, + container: { + width: '85%', + maxWidth: 400, + backgroundColor: '#1E1E1E', + borderRadius: 16, + padding: 24, + borderWidth: 1, + borderColor: 'rgba(255, 255, 255, 0.1)', + }, + title: { + color: '#FFFFFF', + fontSize: 20, + fontWeight: '700', + marginBottom: 8, + textAlign: 'center', + }, + message: { + color: '#AAAAAA', + fontSize: 15, + marginBottom: 20, + textAlign: 'center', + lineHeight: 22, + }, + input: { + backgroundColor: 'rgba(255, 255, 255, 0.08)', + borderRadius: 12, + paddingHorizontal: 16, + paddingVertical: 14, + fontSize: 18, + color: '#FFFFFF', + textAlign: 'center', + marginBottom: 20, + letterSpacing: 4, + }, + buttonRow: { + flexDirection: 'row', + gap: 12, + }, + cancelButton: { + flex: 1, + paddingVertical: 14, + borderRadius: 12, + backgroundColor: 'rgba(255, 255, 255, 0.08)', + alignItems: 'center', + }, + cancelButtonText: { + color: '#FFFFFF', + fontSize: 16, + fontWeight: '600', + }, + confirmButton: { + flex: 1, + paddingVertical: 14, + borderRadius: 12, + alignItems: 'center', + }, + confirmButtonText: { + color: '#FFFFFF', + fontSize: 16, + fontWeight: '600', + }, +}); + export default AboutSettingsScreen; diff --git a/src/screens/settings/DeveloperSettingsScreen.tsx b/src/screens/settings/DeveloperSettingsScreen.tsx index 5ce51fe..f973945 100644 --- a/src/screens/settings/DeveloperSettingsScreen.tsx +++ b/src/screens/settings/DeveloperSettingsScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { View, StyleSheet, ScrollView, StatusBar } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; @@ -18,11 +18,25 @@ const DeveloperSettingsScreen: React.FC = () => { const { t } = useTranslation(); const insets = useSafeAreaInsets(); + const [developerModeEnabled, setDeveloperModeEnabled] = useState(__DEV__); const [alertVisible, setAlertVisible] = useState(false); const [alertTitle, setAlertTitle] = useState(''); const [alertMessage, setAlertMessage] = useState(''); const [alertActions, setAlertActions] = useState void }>>([]); + // Load developer mode state on mount + useEffect(() => { + const loadDevModeState = async () => { + try { + const devModeEnabled = await mmkvStorage.getItem('developer_mode_enabled'); + setDeveloperModeEnabled(__DEV__ || devModeEnabled === 'true'); + } catch (error) { + console.error('Failed to load developer mode state:', error); + } + }; + loadDevModeState(); + }, []); + const openAlert = ( title: string, message: string, @@ -78,8 +92,8 @@ const DeveloperSettingsScreen: React.FC = () => { ); }; - // Only show in development mode - if (!__DEV__) { + // Only show if developer mode is enabled (via __DEV__ or manually unlocked) + if (!developerModeEnabled) { return null; }