fix(android): photo access
This commit is contained in:
parent
fd6ba1caab
commit
35753e0f53
6 changed files with 137 additions and 3 deletions
|
@ -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"/>
|
||||||
|
|
|
@ -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
|
|
@ -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",
|
||||||
|
|
122
src/permissions/mediaPermissions.js
Normal file
122
src/permissions/mediaPermissions.js
Normal 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,
|
||||||
|
};
|
|
@ -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,
|
||||||
|
|
|
@ -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";
|
||||||
|
|
Loading…
Add table
Reference in a new issue