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.ACTIVITY_RECOGNITION"/>
|
||||
<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.MODIFY_AUDIO_SETTINGS"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<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.RECORD_AUDIO"/>
|
||||
<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
|
||||
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
|
|
@ -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",
|
||||
|
|
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 { 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,
|
||||
|
|
|
@ -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";
|
||||
|
|
Loading…
Add table
Reference in a new issue