diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 1fa150c..dff6951 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -4,11 +4,13 @@
+
+
diff --git a/android/gradle.properties b/android/gradle.properties
index bceb108..76f893d 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -67,5 +67,6 @@ EX_UPDATES_ANDROID_DELAY_LOAD_APP=false
# Ensure we always pick the NDK r27 libc++_shared.so we vendor in jniLibs
android.packagingOptions.pickFirsts=**/libc++_shared.so
+
# Whether the app is configured to use edge-to-edge via the app config or `react-native-edge-to-edge` plugin
-expo.edgeToEdgeEnabled=false
+expo.edgeToEdgeEnabled=false
\ No newline at end of file
diff --git a/app.config.js b/app.config.js
index 09fc17a..236bb3b 100644
--- a/app.config.js
+++ b/app.config.js
@@ -60,10 +60,12 @@ let config = {
"android.permission.ACCESS_COARSE_LOCATION",
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.CALL_PHONE",
+ "android.permission.CAMERA",
"android.permission.INTERNET",
"android.permission.MODIFY_AUDIO_SETTINGS",
"android.permission.READ_CONTACTS",
"android.permission.READ_EXTERNAL_STORAGE",
+ "android.permission.READ_MEDIA_IMAGES",
"android.permission.RECORD_AUDIO",
"android.permission.SYSTEM_ALERT_WINDOW",
"android.permission.VIBRATE",
diff --git a/src/permissions/mediaPermissions.js b/src/permissions/mediaPermissions.js
new file mode 100644
index 0000000..8042b43
--- /dev/null
+++ b/src/permissions/mediaPermissions.js
@@ -0,0 +1,122 @@
+import { Alert, Platform } from "react-native";
+import {
+ check,
+ request,
+ openSettings,
+ PERMISSIONS,
+ RESULTS,
+} from "react-native-permissions";
+
+/**
+ * Show an alert inviting the user to open the OS settings when permission is blocked.
+ */
+const promptOpenSettings = (title, message) =>
+ new Promise((resolve) => {
+ Alert.alert(title, message, [
+ { text: "Annuler", style: "cancel", onPress: () => resolve(false) },
+ {
+ text: "Ouvrir les réglages",
+ onPress: () => {
+ openSettings().catch(() => {});
+ resolve(false);
+ },
+ },
+ ]);
+ });
+
+/**
+ * Generic helper to check/request a single permission and handle blocked state.
+ */
+const ensurePermission = async (permission, niceName) => {
+ try {
+ const status = await check(permission);
+
+ if (status === RESULTS.GRANTED || status === RESULTS.LIMITED) {
+ return true;
+ }
+
+ if (status === RESULTS.BLOCKED) {
+ await promptOpenSettings(
+ `Permission ${niceName} requise`,
+ `Veuillez autoriser l'accès ${niceName} dans les réglages de l'application.`,
+ );
+ return false;
+ }
+
+ const req = await request(permission);
+
+ if (req === RESULTS.BLOCKED) {
+ await promptOpenSettings(
+ `Permission ${niceName} requise`,
+ `Veuillez autoriser l'accès ${niceName} dans les réglages de l'application.`,
+ );
+ return false;
+ }
+
+ return req === RESULTS.GRANTED || req === RESULTS.LIMITED;
+ } catch (e) {
+ console.warn(`Failed to request ${niceName} permission`, e);
+ return false;
+ }
+};
+
+/**
+ * Ensure camera permission.
+ */
+export const ensureCameraPermission = async () => {
+ const perm =
+ Platform.OS === "android"
+ ? PERMISSIONS.ANDROID.CAMERA
+ : PERMISSIONS.IOS.CAMERA;
+
+ return ensurePermission(perm, "à la caméra");
+};
+
+/**
+ * Ensure photo library / media images read permission.
+ * On Android:
+ * - API >= 33: READ_MEDIA_IMAGES
+ * - API < 33: READ_EXTERNAL_STORAGE
+ * On iOS: PHOTO_LIBRARY (LIMITED is accepted).
+ */
+export const ensurePhotoPermission = async () => {
+ if (Platform.OS === "android") {
+ // Coerce API level to number to avoid misclassification on some devices
+ const apiLevel = Number(Platform.Version);
+ const isApi33Plus = !Number.isNaN(apiLevel) && apiLevel >= 33;
+
+ const primary = isApi33Plus
+ ? PERMISSIONS.ANDROID.READ_MEDIA_IMAGES
+ : PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE;
+ const secondary = isApi33Plus
+ ? PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE
+ : PERMISSIONS.ANDROID.READ_MEDIA_IMAGES;
+
+ // Try primary permission first
+ const primaryGranted = await ensurePermission(primary, "à vos photos");
+ if (primaryGranted) return true;
+
+ // If primary failed and secondary is not explicitly blocked, try secondary as a fallback
+ try {
+ const statusSecondary = await check(secondary);
+ if (statusSecondary !== RESULTS.BLOCKED) {
+ const secondaryGranted = await ensurePermission(
+ secondary,
+ "à vos photos",
+ );
+ if (secondaryGranted) return true;
+ }
+ } catch (e) {
+ // ignore and fall through
+ }
+
+ return false;
+ }
+
+ return ensurePermission(PERMISSIONS.IOS.PHOTO_LIBRARY, "à vos photos");
+};
+
+export default {
+ ensureCameraPermission,
+ ensurePhotoPermission,
+};
diff --git a/src/scenes/Profile/AvatarModalEdit.js b/src/scenes/Profile/AvatarModalEdit.js
index 92a43f7..2ebcc12 100644
--- a/src/scenes/Profile/AvatarModalEdit.js
+++ b/src/scenes/Profile/AvatarModalEdit.js
@@ -6,6 +6,10 @@ import ImagePicker from "react-native-image-crop-picker";
import { createStyles, useTheme } from "~/theme";
import { useFormContext } from "react-hook-form";
import ImageResizer from "@bam.tech/react-native-image-resizer";
+import {
+ ensureCameraPermission,
+ ensurePhotoPermission,
+} from "~/permissions/mediaPermissions";
import env from "~/env";
@@ -83,8 +87,12 @@ export default function AvatarModalEdit({ modalState, userId }) {
try {
let pickedImage;
if (mode === "library") {
+ const granted = await ensurePhotoPermission();
+ if (!granted) return;
pickedImage = await ImagePicker.openPicker(options);
} else if (mode === "camera") {
+ const granted = await ensureCameraPermission();
+ if (!granted) return;
pickedImage = await ImagePicker.openCamera({
...options,
useFrontCamera: true,
diff --git a/src/scenes/Profile/AvatarUploader.js b/src/scenes/Profile/AvatarUploader.js
index a101205..6284331 100644
--- a/src/scenes/Profile/AvatarUploader.js
+++ b/src/scenes/Profile/AvatarUploader.js
@@ -2,8 +2,7 @@ import React, { useCallback, useState } from "react";
import { View, Image, TouchableWithoutFeedback } from "react-native";
import { IconButton, Avatar } from "react-native-paper";
import { MaterialCommunityIcons } from "@expo/vector-icons";
-import ImagePicker from "react-native-image-crop-picker";
-import { createStyles, useTheme } from "~/theme";
+import { useTheme } from "~/theme";
import { useFormContext } from "react-hook-form";
// import Text from "~/components/Text";
import ImageResizer from "@bam.tech/react-native-image-resizer";