fix(android): photo access

This commit is contained in:
devthejo 2025-10-01 12:44:30 +02:00
parent fd6ba1caab
commit 35753e0f53
No known key found for this signature in database
GPG key ID: 00CCA7A92B1D5351
6 changed files with 137 additions and 3 deletions

View file

@ -4,11 +4,13 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION"/> <uses-permission android:name="android.permission.ACTIVITY_RECOGNITION"/>
<uses-permission android:name="android.permission.CALL_PHONE"/> <uses-permission android:name="android.permission.CALL_PHONE"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/> <uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>

View file

@ -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 # Ensure we always pick the NDK r27 libc++_shared.so we vendor in jniLibs
android.packagingOptions.pickFirsts=**/libc++_shared.so 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 # 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

View file

@ -60,10 +60,12 @@ let config = {
"android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION",
"android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_FINE_LOCATION",
"android.permission.CALL_PHONE", "android.permission.CALL_PHONE",
"android.permission.CAMERA",
"android.permission.INTERNET", "android.permission.INTERNET",
"android.permission.MODIFY_AUDIO_SETTINGS", "android.permission.MODIFY_AUDIO_SETTINGS",
"android.permission.READ_CONTACTS", "android.permission.READ_CONTACTS",
"android.permission.READ_EXTERNAL_STORAGE", "android.permission.READ_EXTERNAL_STORAGE",
"android.permission.READ_MEDIA_IMAGES",
"android.permission.RECORD_AUDIO", "android.permission.RECORD_AUDIO",
"android.permission.SYSTEM_ALERT_WINDOW", "android.permission.SYSTEM_ALERT_WINDOW",
"android.permission.VIBRATE", "android.permission.VIBRATE",

View file

@ -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,
};

View file

@ -6,6 +6,10 @@ import ImagePicker from "react-native-image-crop-picker";
import { createStyles, useTheme } from "~/theme"; import { createStyles, useTheme } from "~/theme";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
import ImageResizer from "@bam.tech/react-native-image-resizer"; import ImageResizer from "@bam.tech/react-native-image-resizer";
import {
ensureCameraPermission,
ensurePhotoPermission,
} from "~/permissions/mediaPermissions";
import env from "~/env"; import env from "~/env";
@ -83,8 +87,12 @@ export default function AvatarModalEdit({ modalState, userId }) {
try { try {
let pickedImage; let pickedImage;
if (mode === "library") { if (mode === "library") {
const granted = await ensurePhotoPermission();
if (!granted) return;
pickedImage = await ImagePicker.openPicker(options); pickedImage = await ImagePicker.openPicker(options);
} else if (mode === "camera") { } else if (mode === "camera") {
const granted = await ensureCameraPermission();
if (!granted) return;
pickedImage = await ImagePicker.openCamera({ pickedImage = await ImagePicker.openCamera({
...options, ...options,
useFrontCamera: true, useFrontCamera: true,

View file

@ -2,8 +2,7 @@ import React, { useCallback, useState } from "react";
import { View, Image, TouchableWithoutFeedback } from "react-native"; import { View, Image, TouchableWithoutFeedback } from "react-native";
import { IconButton, Avatar } from "react-native-paper"; import { IconButton, Avatar } from "react-native-paper";
import { MaterialCommunityIcons } from "@expo/vector-icons"; import { MaterialCommunityIcons } from "@expo/vector-icons";
import ImagePicker from "react-native-image-crop-picker"; import { useTheme } from "~/theme";
import { createStyles, useTheme } from "~/theme";
import { useFormContext } from "react-hook-form"; import { useFormContext } from "react-hook-form";
// import Text from "~/components/Text"; // import Text from "~/components/Text";
import ImageResizer from "@bam.tech/react-native-image-resizer"; import ImageResizer from "@bam.tech/react-native-image-resizer";