Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: request permissions before using camera/media library in image picker #1398

Merged
merged 4 commits into from
Jul 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dev-client/.depcheckrc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"eslint-plugin-prettier",
"expo-crypto",
"expo-dev-client",
"expo-modules-core",
"metro-react-native-babel-preset",
"react-devtools",
"react-native-gradle-plugin",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
launchCameraAsync,
launchImageLibraryAsync,
MediaTypeOptions,
useCameraPermissions,
useMediaLibraryPermissions,
} from 'expo-image-picker';
import {createAssetAsync} from 'expo-media-library';

Expand All @@ -35,6 +37,7 @@ import {
ModalHandle,
ModalTrigger,
} from 'terraso-mobile-client/components/modals/Modal';
import {PermissionsRequestModal} from 'terraso-mobile-client/components/modals/PermissionsRequestModal';
import {Column} from 'terraso-mobile-client/components/NativeBaseAdapters';
import {OverlaySheet} from 'terraso-mobile-client/components/sheets/OverlaySheet';

Expand All @@ -56,10 +59,16 @@ export const decodeBase64Jpg = (base64: string) =>

type Props = {
onPick: (result: Photo) => void;
featureName: string;
children: ModalTrigger;
};

export const ImagePicker = ({onPick, children, ...modalProps}: Props) => {
export const ImagePicker = ({
onPick,
children,
featureName,
...modalProps
}: Props) => {
const {t} = useTranslation();
const ref = useRef<ModalHandle>(null);

Expand All @@ -74,35 +83,51 @@ export const ImagePicker = ({onPick, children, ...modalProps}: Props) => {
onPick(response.assets[0]);
}
ref.current?.onClose();
}, [onPick, ref]);
}, [onPick]);

const onUseGallery = useCallback(async () => {
const onUseMediaLibrary = useCallback(async () => {
const response = await launchImageLibraryAsync({
mediaTypes: MediaTypeOptions.Images,
});
if (!response.canceled) {
onPick(response.assets[0]);
}
ref.current?.onClose();
}, [onPick, ref]);
}, [onPick]);

const onCancel = useCallback(() => ref.current?.onClose(), [ref]);
const onCancel = useCallback(() => ref.current?.onClose(), []);

return (
<OverlaySheet ref={ref} trigger={children} Closer={null} {...modalProps}>
<Column padding="lg" space="md">
<Button
_text={{textTransform: 'uppercase'}}
onPress={onUseCamera}
rightIcon={<Icon name="photo-camera" />}>
{t('image.use_camera')}
</Button>
<Button
_text={{textTransform: 'uppercase'}}
onPress={onUseGallery}
rightIcon={<Icon name="photo-library" />}>
{t('image.choose_from_gallery')}
</Button>
<PermissionsRequestModal
title={t('permissions.camera_title')}
body={t('permissions.camera_body', {feature: featureName})}
usePermissions={useCameraPermissions}
permissionedAction={onUseCamera}>
{onRequestAction => (
<Button
_text={{textTransform: 'uppercase'}}
onPress={onRequestAction}
rightIcon={<Icon name="photo-camera" />}>
{t('image.use_camera')}
</Button>
)}
</PermissionsRequestModal>
<PermissionsRequestModal
title={t('permissions.gallery_title')}
body={t('permissions.gallery_body', {feature: featureName})}
usePermissions={useMediaLibraryPermissions}
permissionedAction={onUseMediaLibrary}>
{onRequestAction => (
<Button
_text={{textTransform: 'uppercase'}}
onPress={onRequestAction}
rightIcon={<Icon name="photo-library" />}>
{t('image.choose_from_gallery')}
</Button>
)}
</PermissionsRequestModal>
<Button
_text={{textTransform: 'uppercase'}}
variant="outline"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,20 @@
import {Pressable} from 'react-native';

import {Icon} from 'terraso-mobile-client/components/icons/Icon';
import {ImagePicker, Photo} from 'terraso-mobile-client/components/ImagePicker';
import {
ImagePicker,
Photo,
} from 'terraso-mobile-client/components/inputs/image/ImagePicker';
import {Box} from 'terraso-mobile-client/components/NativeBaseAdapters';

type Props = {
featureName: string;
onPick: (photo: Photo) => void;
};

export const PickImageButton = ({onPick}: Props) => {
export const PickImageButton = ({featureName, onPick}: Props) => {
return (
<ImagePicker onPick={onPick}>
<ImagePicker featureName={featureName} onPick={onPick}>
{onOpen => (
<Pressable onPress={onOpen}>
<Box
Expand Down
80 changes: 80 additions & 0 deletions dev-client/src/components/modals/PermissionsRequestModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright © 2024 Technology Matters
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/

import {useCallback, useRef} from 'react';
import {useTranslation} from 'react-i18next';
import {Linking} from 'react-native';

import {createPermissionHook} from 'expo-modules-core';

import {ConfirmModal} from 'terraso-mobile-client/components/modals/ConfirmModal';
import {ModalHandle} from 'terraso-mobile-client/components/modals/Modal';

type PermissionHook = ReturnType<typeof createPermissionHook>;

type Props = {
title: string;
body: string;
usePermissions: PermissionHook;
permissionedAction?: () => void;
children: (onOpen: () => void) => React.ReactNode;
};

export const PermissionsRequestModal = ({
title,
body,
usePermissions,
permissionedAction,
children,
}: Props) => {
const {t} = useTranslation();
const ref = useRef<ModalHandle>(null);
const [permissions, requestPermissions] = usePermissions();

const onRequestAction = useCallback(async () => {
if (permissions === null) {
return;
}
shrouxm marked this conversation as resolved.
Show resolved Hide resolved

if (permissions.granted) {
if (permissionedAction !== undefined) {
permissionedAction();
}
} else if (permissions.canAskAgain) {
const result = await requestPermissions();
if (result.granted && permissionedAction !== undefined) {
permissionedAction();
}
} else {
ref.current?.onOpen();
}
}, [permissionedAction, permissions, requestPermissions]);

return (
<>
<ConfirmModal
ref={ref}
isConfirmError={false}
actionName={t('general.open_settings')}
title={title}
body={body}
handleConfirm={Linking.openSettings}
/>
{children(onRequestAction)}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {IconButton} from 'terraso-mobile-client/components/icons/IconButton';
import {
decodeBase64Jpg,
PhotoWithBase64,
} from 'terraso-mobile-client/components/ImagePicker';
} from 'terraso-mobile-client/components/inputs/image/ImagePicker';
import {
ActionButton,
ActionsModal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import {useMemo, useState} from 'react';

import {Photo} from 'terraso-mobile-client/components/ImagePicker';
import {Photo} from 'terraso-mobile-client/components/inputs/image/ImagePicker';
import {DEFAULT_STACK_NAVIGATOR_OPTIONS} from 'terraso-mobile-client/navigation/constants';
import {
ColorAnalysisContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {Icon} from 'terraso-mobile-client/components/icons/Icon';
import {
Photo,
PhotoWithBase64,
} from 'terraso-mobile-client/components/ImagePicker';
} from 'terraso-mobile-client/components/inputs/image/ImagePicker';
import {
Box,
Column,
Expand Down
32 changes: 15 additions & 17 deletions dev-client/src/screens/SlopeScreen/SlopeMeterScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,23 @@

import {useCallback, useEffect, useState} from 'react';
import {useTranslation} from 'react-i18next';
import {Linking} from 'react-native';

import {CameraView, useCameraPermissions} from 'expo-camera';
import * as ScreenOrientation from 'expo-screen-orientation';
import {DeviceMotion} from 'expo-sensors';

import {Button, Link} from 'native-base';
import {Button} from 'native-base';

import {updateSoilData} from 'terraso-client-shared/soilId/soilIdSlice';

import {BigCloseButton} from 'terraso-mobile-client/components/buttons/BigCloseButton';
import {Icon} from 'terraso-mobile-client/components/icons/Icon';
import {PermissionsRequestModal} from 'terraso-mobile-client/components/modals/PermissionsRequestModal';
import {
Box,
Column,
Heading,
Row,
Text,
} from 'terraso-mobile-client/components/NativeBaseAdapters';
import {InfoOverlaySheetButton} from 'terraso-mobile-client/components/sheets/InfoOverlaySheetButton';
import {useNavigation} from 'terraso-mobile-client/navigation/hooks/useNavigation';
Expand All @@ -47,7 +46,7 @@ const toDegrees = (rad: number) => Math.round(Math.abs((rad * 180) / Math.PI));

export const SlopeMeterScreen = ({siteId}: {siteId: string}) => {
const {t} = useTranslation();
const [permission, requestPermission] = useCameraPermissions();
const [permission] = useCameraPermissions();
const [deviceTiltDeg, setDeviceTiltDeg] = useState<number | null>(null);
const navigation = useNavigation();
const dispatch = useDispatch();
Expand Down Expand Up @@ -100,20 +99,19 @@ export const SlopeMeterScreen = ({siteId}: {siteId: string}) => {
<Box flex={1} bg="#00000080" />
</Column>
</CameraView>
) : permission?.canAskAgain ? (
<Button size="lg" onPress={requestPermission}>
{t('slope.steepness.camera_grant')}
</Button>
) : (
<>
<Heading variant="h6">{t('slope.steepness.no_camera')}</Heading>
<Text variant="body1" textAlign="center">
{t('slope.steepness.no_camera_explanation')}
</Text>
<Link onPress={Linking.openSettings}>
{t('general.open_settings')}
</Link>
</>
<PermissionsRequestModal
title={t('permissions.camera_title')}
body={t('permissions.camera_body', {
feature: t('slope.steepness.slope_meter'),
})}
usePermissions={useCameraPermissions}>
{onRequest => (
<Button size="lg" onPress={onRequest}>
{t('slope.steepness.camera_grant')}
</Button>
)}
</PermissionsRequestModal>
)}
</Box>
<Column alignItems="center">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ import {Button} from 'native-base';

import {BulletList} from 'terraso-mobile-client/components/BulletList';
import {Icon} from 'terraso-mobile-client/components/icons/Icon';
import {ImagePicker, Photo} from 'terraso-mobile-client/components/ImagePicker';
import {
ImagePicker,
Photo,
} from 'terraso-mobile-client/components/inputs/image/ImagePicker';
import {
Box,
Column,
Expand Down Expand Up @@ -125,7 +128,9 @@ export const ColorGuideScreen = (props: SoilPitInputScreenProps) => {
<Button variant="link" onPress={onGoBack}>
{t('soil.color.guide.go_back')}
</Button>
<ImagePicker onPick={onTakePhoto}>
<ImagePicker
featureName={t('soil.color.featureName')}
onPick={onTakePhoto}>
{onOpen => (
<Button
_text={{textTransform: 'uppercase'}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import {useTranslation} from 'react-i18next';
import {Button} from 'native-base';

import {Icon} from 'terraso-mobile-client/components/icons/Icon';
import {Photo} from 'terraso-mobile-client/components/ImagePicker';
import {PickImageButton} from 'terraso-mobile-client/components/inputs/PickImageButton';
import {Photo} from 'terraso-mobile-client/components/inputs/image/ImagePicker';
import {PickImageButton} from 'terraso-mobile-client/components/inputs/image/PickImageButton';
import {
Box,
Column,
Expand Down Expand Up @@ -53,7 +53,10 @@ export const CameraWorkflow = (props: SoilPitInputScreenProps) => {
return (
<Column>
<Box alignItems="center" paddingVertical="lg">
<PickImageButton onPick={onPickImage} />
<PickImageButton
featureName={t('soil.color.featureName')}
onPick={onPickImage}
/>
</Box>
<Column
backgroundColor="grey.300"
Expand Down
9 changes: 8 additions & 1 deletion dev-client/src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -435,12 +435,18 @@
},
"required": "Required",
"character_limit": "{{current}} / {{limit}} characters",
"open_settings": "Open Settings",
"open_settings": "Go to settings",
"yes": "Yes",
"no": "No",
"next": "Next",
"proceed": "Proceed"
},
"permissions": {
"camera_title": "LandPKS Soil ID needs to access your camera",
"camera_body": "To use {{feature}}, grant LandPKS Soil ID access to your camera in device settings.",
"gallery_title": "LandPKS Soil ID needs to access your photo gallery",
"gallery_body": "To use {{feature}}, grant LandPKS Soil ID access to your photo gallery in device settings."
},
"errors": {
"generic": "Error saving soil ID information."
},
Expand Down Expand Up @@ -715,6 +721,7 @@
},
"color": {
"title": "Soil Color",
"featureName": "Soil Color",
"info": {
"p1": "Soil color can tell you a lot about soil, including:",
"bullet1": "Which minerals are present",
Expand Down
Loading