diff --git a/.gitignore b/.gitignore index f045abd..eff32d4 100644 --- a/.gitignore +++ b/.gitignore @@ -57,7 +57,7 @@ android/app/release/ node_modules/ npm-debug.log yarn-error.log -# package-lock.json +package-lock.json android/app/google-services.json android/keystore.properties diff --git a/App.tsx b/App.tsx index 6cce5e7..3088b2b 100644 --- a/App.tsx +++ b/App.tsx @@ -1,7 +1,10 @@ import React, {useEffect} from 'react'; -// import 'react-native-reanimated'; - -import {StatusBar, StyleSheet} from 'react-native'; +import { + StatusBar, + StyleSheet, + PermissionsAndroid, + Platform, +} from 'react-native'; import {SafeAreaProvider, SafeAreaView} from 'react-native-safe-area-context'; import Toast from 'react-native-toast-message'; import MainApp from './src/main'; @@ -14,8 +17,43 @@ enableScreens(); function App(): React.JSX.Element { useEffect(() => { SplashScreen.hide(); + requestStoragePermission(); }, []); + const requestStoragePermission = async () => { + try { + if (Platform.OS === 'android') { + if (Platform.Version >= 33) { + // Android 13+ (Images, Video, Audio separately) + const result = await PermissionsAndroid.requestMultiple([ + PermissionsAndroid.PERMISSIONS.READ_MEDIA_IMAGES, + PermissionsAndroid.PERMISSIONS.READ_MEDIA_VIDEO, + PermissionsAndroid.PERMISSIONS.READ_MEDIA_AUDIO, + ]); + console.log('Android 13+ permission result:', result); + } else { + // Android 12 and below + const granted = await PermissionsAndroid.request( + PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE, + { + title: 'Storage Permission Required', + message: + 'ScheduleX needs access to your storage to function properly.', + buttonPositive: 'OK', + }, + ); + if (granted === PermissionsAndroid.RESULTS.GRANTED) { + console.log('Storage permission granted'); + } else { + console.log('Storage permission denied'); + } + } + } + } catch (err) { + console.warn(err); + } + }; + return ( @@ -44,4 +82,5 @@ const styles = StyleSheet.create({ backgroundColor: '#18181B', }, }); + export default App; diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7bbaa5b..8ee462b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,31 +1,52 @@ - + + + + + + + + + + - - - - - - - - - + android:icon="@mipmap/ic_launcher" + android:roundIcon="@mipmap/ic_launcher_round" + android:allowBackup="false" + android:theme="@style/AppTheme" + android:supportsRtl="true"> + + - \ No newline at end of file + + + + + + + + + + + + + diff --git a/android/app/src/main/res/xml/file_paths.xml b/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..a6ce6d3 --- /dev/null +++ b/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,4 @@ + + + + diff --git a/package.json b/package.json index d6cfb42..fb28402 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "react-native-linear-gradient": "^2.8.3", "react-native-multiple-select": "^0.5.12", "react-native-pager-view": "^6.8.1", + "react-native-permissions": "^5.4.2", "react-native-safe-area-context": "^5.0.0", "react-native-screens": "^4.3.0", "react-native-share": "^12.1.0", diff --git a/src/components/GlobalRegisterSelector.tsx b/src/components/GlobalRegisterSelector.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/exportSchedule.ts b/src/utils/exportSchedule.ts index e35b7ff..ceeeb78 100644 --- a/src/utils/exportSchedule.ts +++ b/src/utils/exportSchedule.ts @@ -3,6 +3,7 @@ import { Alert, ToastAndroid, Platform, PermissionsAndroid } from 'react-native' import Share from 'react-native-share'; import { generateRegisterCSV } from './csv-export'; import { CardInterface } from '../types/cards'; +import { PermissionsHelper } from './permissions'; export interface ExportResult { path: string; @@ -25,25 +26,9 @@ export class ExportScheduleUtility { } private async checkStoragePermission(): Promise { - if (Platform.OS === 'android') { - try { - const granted = await PermissionsAndroid.request( - PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, - { - title: 'Storage Permission', - message: 'App needs access to storage to save CSV files', - buttonNeutral: 'Ask Me Later', - buttonNegative: 'Cancel', - buttonPositive: 'OK', - } - ); - return granted === PermissionsAndroid.RESULTS.GRANTED; - } catch (err) { - console.warn(err); - return false; - } - } - return true; // iOS doesn't need explicit storage permission for app documents + // Use PermissionsHelper for consistent permission logic + const result = await PermissionsHelper.requestStoragePermission(); + return result.granted; } private getAllRegisterIds(): number[] { diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts index 7ff25dd..e6e6238 100644 --- a/src/utils/permissions.ts +++ b/src/utils/permissions.ts @@ -1,4 +1,136 @@ -import { Platform, PermissionsAndroid, Alert } from 'react-native'; +// import { Platform, Alert } from 'react-native'; +// import { check, request, PERMISSIONS, RESULTS, openSettings } from 'react-native-permissions'; + +// export interface PermissionResult { +// granted: boolean; +// canRequestAgain: boolean; +// } + +// export class PermissionsHelper { + +// /** +// * Check if we need to request storage permissions based on Android version +// * Android 11+ (API 30+) uses scoped storage, so we don't need WRITE_EXTERNAL_STORAGE +// * for app-specific directories +// */ +// static needsStoragePermission(): boolean { +// return Platform.OS === 'android'; +// } + +// /** +// * Request storage permissions for Android +// */ +// static async requestStoragePermission(): Promise { +// if (Platform.OS === 'android') { +// const permission = PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE; +// try { +// const result = await request(permission); +// if (result === RESULTS.GRANTED) { +// return { granted: true, canRequestAgain: false }; +// } else if (result === RESULTS.BLOCKED) { +// return { granted: false, canRequestAgain: false }; +// } else { +// return { granted: false, canRequestAgain: true }; +// } +// } catch (error) { +// console.warn('Permission request error:', error); +// return { granted: false, canRequestAgain: true }; +// } +// } else if (Platform.OS === 'ios') { +// const permission = PERMISSIONS.IOS.PHOTO_LIBRARY; +// try { +// const result = await request(permission); +// if (result === RESULTS.GRANTED) { +// return { granted: true, canRequestAgain: false }; +// } else if (result === RESULTS.BLOCKED) { +// return { granted: false, canRequestAgain: false }; +// } else { +// return { granted: false, canRequestAgain: true }; +// } +// } catch (error) { +// console.warn('Permission request error:', error); +// return { granted: false, canRequestAgain: true }; +// } +// } +// return { granted: true, canRequestAgain: false }; +// } + +// /** +// * Check if storage permissions are currently granted +// */ +// static async checkStoragePermission(): Promise { +// if (Platform.OS === 'android') { +// const permission = PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE; +// try { +// const result = await check(permission); +// return result === RESULTS.GRANTED; +// } catch (error) { +// console.warn('Permission check error:', error); +// return false; +// } +// } else if (Platform.OS === 'ios') { +// const permission = PERMISSIONS.IOS.PHOTO_LIBRARY; +// try { +// const result = await check(permission); +// return result === RESULTS.GRANTED; +// } catch (error) { +// console.warn('Permission check error:', error); +// return false; +// } +// } +// return true; +// } + +// /** +// * Show permission explanation dialog +// */ +// static showPermissionExplanation(onRetry: () => void, onCancel: () => void) { +// Alert.alert( +// 'Storage Permission Required', +// 'This app needs storage permission to save CSV files to your device. You can still use the share feature without this permission.', +// [ +// { +// text: 'Cancel', +// style: 'cancel', +// onPress: onCancel, +// }, +// { +// text: 'Grant Permission', +// onPress: onRetry, +// }, +// ] +// ); +// } + +// /** +// * Show permission denied dialog +// */ +// static showPermissionDenied(onUseAppStorage: () => void) { +// Alert.alert( +// 'Permission Denied', +// 'Storage permission was denied. You can either:\n\n1. Enable it in Settings\n2. Use app storage (files will be saved in app folder)', +// [ +// { +// text: 'Use App Storage', +// onPress: onUseAppStorage, +// }, +// { +// text: 'Open Settings', +// onPress: () => openSettings(), +// }, +// ] +// ); +// } +// } + +import { Platform, Alert } from "react-native"; +import { + check, + request, + PERMISSIONS, + RESULTS, + openSettings, +} from "react-native-permissions"; export interface PermissionResult { granted: boolean; @@ -6,96 +138,84 @@ export interface PermissionResult { } export class PermissionsHelper { - /** * Check if we need to request storage permissions based on Android version - * Android 11+ (API 30+) uses scoped storage, so we don't need WRITE_EXTERNAL_STORAGE - * for app-specific directories */ static needsStoragePermission(): boolean { - if (Platform.OS !== 'android') return false; - - // For Android 11+ (API 30+), we use app-scoped storage which doesn't require permissions - const androidVersion = Platform.Version as number; - return androidVersion < 30; + return Platform.OS === "android"; } /** - * Request storage permissions for Android + * Request storage permissions for Android & iOS */ - static async requestStoragePermission(forPublicStorage: boolean = false): Promise { - if (Platform.OS !== 'android') { - return { granted: true, canRequestAgain: false }; - } + static async requestStoragePermission(): Promise { + if (Platform.OS === "android") { + // For Android 11+, rely on Document Picker instead of MANAGE_EXTERNAL_STORAGE + const permission = + Platform.Version >= 30 + ? PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE + : PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE; - // Android 11+ doesn't need storage permissions for app-scoped directories - if (!this.needsStoragePermission() && !forPublicStorage) { - return { granted: true, canRequestAgain: false }; - } - - try { - // For Android 10 and below, or if explicitly requesting public storage - if (this.needsStoragePermission() || forPublicStorage) { - const writeResult = await PermissionsAndroid.request( - PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, - { - title: 'Storage Permission', - message: 'App needs storage permission to save CSV files', - buttonNeutral: 'Ask Me Later', - buttonNegative: 'Cancel', - buttonPositive: 'OK', - } - ); - - const readResult = await PermissionsAndroid.request( - PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE, - { - title: 'Storage Permission', - message: 'App needs storage permission to access files', - buttonNeutral: 'Ask Me Later', - buttonNegative: 'Cancel', - buttonPositive: 'OK', - } - ); - - const allGranted = writeResult === PermissionsAndroid.RESULTS.GRANTED && - readResult === PermissionsAndroid.RESULTS.GRANTED; - - const canRequestAgain = writeResult !== PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN && - readResult !== PermissionsAndroid.RESULTS.NEVER_ASK_AGAIN; - - return { - granted: allGranted, - canRequestAgain - }; + try { + const result = await request(permission); + if (result === RESULTS.GRANTED) { + return { granted: true, canRequestAgain: false }; + } else if (result === RESULTS.BLOCKED) { + return { granted: false, canRequestAgain: false }; + } else { + return { granted: false, canRequestAgain: true }; + } + } catch (error) { + console.warn("Permission request error:", error); + return { granted: false, canRequestAgain: true }; + } + } else if (Platform.OS === "ios") { + const permission = PERMISSIONS.IOS.PHOTO_LIBRARY_ADD_ONLY; + try { + const result = await request(permission); + if (result === RESULTS.GRANTED) { + return { granted: true, canRequestAgain: false }; + } else if (result === RESULTS.BLOCKED) { + return { granted: false, canRequestAgain: false }; + } else { + return { granted: false, canRequestAgain: true }; + } + } catch (error) { + console.warn("Permission request error:", error); + return { granted: false, canRequestAgain: true }; } - - return { granted: true, canRequestAgain: false }; - - } catch (error) { - console.warn('Permission request error:', error); - return { granted: false, canRequestAgain: true }; } + return { granted: true, canRequestAgain: false }; } /** * Check if storage permissions are currently granted */ static async checkStoragePermission(): Promise { - if (Platform.OS !== 'android') return true; - - if (!this.needsStoragePermission()) return true; + if (Platform.OS === "android") { + const permission = + Platform.Version >= 30 + ? PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE + : PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE; - try { - const writePermission = await PermissionsAndroid.check( - PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE - ); - - return writePermission; - } catch (error) { - console.warn('Permission check error:', error); - return false; + try { + const result = await check(permission); + return result === RESULTS.GRANTED; + } catch (error) { + console.warn("Permission check error:", error); + return false; + } + } else if (Platform.OS === "ios") { + const permission = PERMISSIONS.IOS.PHOTO_LIBRARY_ADD_ONLY; + try { + const result = await check(permission); + return result === RESULTS.GRANTED; + } catch (error) { + console.warn("Permission check error:", error); + return false; + } } + return true; } /** @@ -103,16 +223,16 @@ export class PermissionsHelper { */ static showPermissionExplanation(onRetry: () => void, onCancel: () => void) { Alert.alert( - 'Storage Permission Required', - 'This app needs storage permission to save CSV files to your device. You can still use the share feature without this permission.', + "Storage Permission Required", + "This app needs storage permission to save CSV files to your device. You can still use the share feature without this permission.", [ { - text: 'Cancel', - style: 'cancel', + text: "Cancel", + style: "cancel", onPress: onCancel, }, { - text: 'Grant Permission', + text: "Grant Permission", onPress: onRetry, }, ] @@ -122,18 +242,18 @@ export class PermissionsHelper { /** * Show permission denied dialog */ - static showPermissionDenied(onOpenSettings: () => void, onUseAppStorage: () => void) { + static showPermissionDenied(onUseAppStorage: () => void) { Alert.alert( - 'Permission Denied', - 'Storage permission was denied. You can either:\n\n1. Enable it in Settings\n2. Use app storage (files will be saved in app folder)', + "Permission Denied", + "Storage permission was denied. You can either:\n\n1. Enable it in Settings\n2. Use app storage (files will be saved in app folder)", [ { - text: 'Use App Storage', + text: "Use App Storage", onPress: onUseAppStorage, }, { - text: 'Open Settings', - onPress: onOpenSettings, + text: "Open Settings", + onPress: () => openSettings(), }, ] ); diff --git a/src/utils/storage-permissions.ts b/src/utils/storage-permissions.ts new file mode 100644 index 0000000..97b55b7 --- /dev/null +++ b/src/utils/storage-permissions.ts @@ -0,0 +1,6 @@ +import { PermissionsHelper } from './permissions'; + +export const requestStoragePermission = async (): Promise => { + const result = await PermissionsHelper.requestStoragePermission(); + return result.granted; +}; diff --git a/yarn.lock b/yarn.lock index 5a0104d..f6c30e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6778,7 +6778,7 @@ react-native-dotenv@^3.4.11: react-native-fs@^2.20.0: version "2.20.0" - resolved "https://registry.npmjs.org/react-native-fs/-/react-native-fs-2.20.0.tgz" + resolved "https://registry.yarnpkg.com/react-native-fs/-/react-native-fs-2.20.0.tgz#05a9362b473bfc0910772c0acbb73a78dbc810f6" integrity sha512-VkTBzs7fIDUiy/XajOSNk0XazFE9l+QlMAce7lGuebZcag5CnjszB+u4BdqzwaQOdcYb5wsJIsqq4kxInIRpJQ== dependencies: base-64 "^0.1.0" @@ -6830,6 +6830,11 @@ react-native-pager-view@^6.8.1: resolved "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.8.1.tgz" integrity sha512-XIyVEMhwq7sZqM7GobOJZXxFCfdFgVNq/CFB2rZIRNRSVPJqE1k1fsc8xfQKfdzsp6Rpt6I7VOIvhmP7/YHdVg== +react-native-permissions@^5.4.2: + version "5.4.2" + resolved "https://registry.yarnpkg.com/react-native-permissions/-/react-native-permissions-5.4.2.tgz#206c0e440cff2ce047514995457bd73a9924fd8b" + integrity sha512-XNMoG1fxrB9q73MLn/ZfTaP7pS8qPu0KWypbeFKVTvoR+JJ3O7uedMOTH/mts9bTG+GKhShOoZ+k0CR63q9jwA== + react-native-safe-area-context@^5.0.0: version "5.5.2" resolved "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.5.2.tgz"