From a52996f5355999cfec9fa95e535eaaa772e67e79 Mon Sep 17 00:00:00 2001 From: Johannes Klein Date: Fri, 12 Jul 2024 11:00:24 +0200 Subject: [PATCH] Changes to the way permissions are asked for (#1793) * Replace name in permission requests * TakePhoto TS * PermissionGate TS * Type * PermissionGateContainer TS * Interface * Types * LocationGate TS * Remove LocationPermissionGate from Camera * Remove write only permission * Type * ObsPhotoSelectionList TS * Code style * Show the improve with location button * Create useLocationPermission.tsx * Use new hook on suggestions * Doc comment * Use new hook in camera view * Add strings * Refactor Explore main content * Use permission hook on RootExplore * Add no location permission component * Rename function * Prop request permissions and use with button * Default to Nearby label * Remove Node type * Projects TS * Use useLocationPermission hook in projects screen * Add string * Prop permission down * Refactor list render * Refactor tab id into enum * Tab type * On nearby tab if without permission show button to prompt * Leftovers * Remove location permission gate from ObsEdit * Use location permission hook on evidence section * SearchBar TS * Do not autoFocus on search bar in location picker. Closes #1743 * Update type * LocationSearch TS * Show location permission gate on location picker's mount * Add location permission to CurrentLocationButton * Remove unused props of Map * Remove unused exports from useMapLocation * Migration * Revert "Show location permission gate on location picker's mount" This reverts commit 30ff75698c53d54d0b14cd2bd629f7155b743bf8. * Add callbacks to useLocationPermission hook * Show location permission ask on Obs Edit * Remove unused string * Reset explore filters should set location always to worldwide * Add helper function to show place text in Explore * Remove unused state of filter modal * Show place text in filters modal with helper * Show location permission button only for Nearby explore state * Add a placeMode state * Do not send placeMode to API * Also treat limited permission as yes * useLocationPermission in ExploreLocationSearch * Refactor to setting place mode Instead of logic based on the translated text of the place_guess string that is stored in ExploreContext, we are switching to an enum state that signifies which mode to show on explore: 1.) Nearby: Filters explore results based on the user's location. This also has a state without location permission that does not query the API. 2.) Place: Filtering by a specific place (as retrieved by /places API). 3.) Worldwide: Retrieve worldwide results, i.e. not having a place filter set. 4.) Map area: Filtering explore results precisely to the map rectangle shown on the explore map. * Remove import from test * Remove export * Use blocked title only for blocked permission asks * Move gallery permission container to Tab navigator as are the others * Add gallery save title * Split location permission explanation into two * Update strings.ftl * Only nav to location picker if permission was not granted * Check permission on app being foregrounded * The location permission part is handled by useLocationPermission * Do not store permission result in hook * Use hasPermission from permissions hook * Update fetchUserLocation.e2e-mock * Move hook one higher * Show user location if permission is given * PermissionGate callbacks should use useCallback * Add permission hook to map usage * Fix test * Update layout to be asserted * Add location permission hook to Explore * Remove console.log * Few TS fixes * Indentation * Remove superficial check * Update Podfile.lock --- ios/Podfile | 2 +- ios/Podfile.lock | 8 +- ios/iNaturalistReactNative/Info.plist | 6 +- src/components/Camera/AICamera/AICamera.js | 2 +- .../Camera/AICamera/AICameraButtons.js | 4 +- .../Buttons/{TakePhoto.js => TakePhoto.tsx} | 13 +- src/components/Camera/CameraWithDevice.tsx | 94 ++++---------- .../Camera/StandardCamera/CameraNavButtons.js | 4 +- .../StandardCamera/CameraOptionsButtons.js | 2 +- src/components/Camera/TabletButtons.js | 2 +- src/components/Explore/Explore.js | 120 +++++++++++------- src/components/Explore/ExploreContainer.js | 59 +++++---- src/components/Explore/Header/Header.js | 4 +- src/components/Explore/MapView.tsx | 8 -- src/components/Explore/Modals/FilterModal.tsx | 46 +++---- .../Explore/RootExploreContainer.js | 49 +++---- .../SearchScreens/ExploreLocationSearch.js | 45 ++++--- .../Explore/helpers/mapParamsToAPI.js | 1 + .../Explore/helpers/placeGuessText.ts | 25 ++++ .../Explore/hooks/useMapLocation.ts | 68 ++-------- src/components/Explore/hooks/useParams.js | 14 +- .../{LocationSearch.js => LocationSearch.tsx} | 31 +++-- .../ObsDetails/DetailsTab/LocationSection.tsx | 1 - src/components/ObsEdit/EvidenceSection.js | 49 ++++--- .../ObsEdit/EvidenceSectionContainer.js | 33 ++--- src/components/PhotoImporter/PhotoGallery.js | 24 +--- .../Projects/{Projects.js => Projects.tsx} | 73 ++++++++--- src/components/Projects/ProjectsContainer.tsx | 50 ++++---- ...sionGate.js => LocationPermissionGate.tsx} | 27 ++-- .../Map/CurrentLocationButton.tsx | 37 ++++-- .../SharedComponents/Map/DetailsMap.js | 1 - src/components/SharedComponents/Map/Map.tsx | 72 ++--------- .../{PermissionGate.js => PermissionGate.tsx} | 31 ++++- ...ntainer.js => PermissionGateContainer.tsx} | 89 +++++++------ .../{SearchBar.js => SearchBar.tsx} | 32 +++-- src/components/SharedComponents/Tabs/Tabs.tsx | 2 +- src/components/SharedComponents/index.js | 1 - ...ctionList.js => ObsPhotoSelectionList.tsx} | 17 +-- src/components/Suggestions/Suggestions.js | 16 +-- .../Suggestions/SuggestionsContainer.tsx | 23 ++-- .../Suggestions/SuggestionsHeader.tsx | 31 +++-- .../TaxonDetails/TaxonMapPreview.js | 1 - src/i18n/l10n/en.ftl | 8 +- src/i18n/l10n/en.ftl.json | 8 +- src/i18n/strings.ftl | 8 +- .../NoBottomTabStackNavigator.js | 22 +++- src/providers/ExploreContext.tsx | 80 +++++++++--- src/realmModels/LocalPreferences.js | 3 +- src/realmModels/index.js | 10 +- src/sharedHelpers/fetchUserLocation.e2e-mock | 2 +- src/sharedHelpers/fetchUserLocation.ts | 7 +- .../useCurrentObservationLocation.js | 16 +-- src/sharedHooks/useLocationPermission.tsx | 108 ++++++++++++++++ src/sharedHooks/useUserLocation.ts | 5 +- tests/integration/ObsEditOffline.test.js | 5 - tests/integration/navigation/Explore.test.js | 8 +- .../components/ObsDetails/DetailsTab.test.js | 3 - .../SharedComponents/PermissionGate.test.js | 2 +- 58 files changed, 810 insertions(+), 702 deletions(-) rename src/components/Camera/Buttons/{TakePhoto.js => TakePhoto.tsx} (91%) create mode 100644 src/components/Explore/helpers/placeGuessText.ts rename src/components/LocationPicker/{LocationSearch.js => LocationSearch.tsx} (81%) rename src/components/Projects/{Projects.js => Projects.tsx} (67%) rename src/components/SharedComponents/{LocationPermissionGate.js => LocationPermissionGate.tsx} (71%) rename src/components/SharedComponents/{PermissionGate.js => PermissionGate.tsx} (82%) rename src/components/SharedComponents/{PermissionGateContainer.js => PermissionGateContainer.tsx} (76%) rename src/components/SharedComponents/{SearchBar.js => SearchBar.tsx} (86%) rename src/components/Suggestions/{ObsPhotoSelectionList.js => ObsPhotoSelectionList.tsx} (83%) create mode 100644 src/sharedHooks/useLocationPermission.tsx diff --git a/ios/Podfile b/ios/Podfile index 725f2b6ad..d2af0c616 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -32,7 +32,7 @@ setup_permissions([ # 'Motion', # 'Notifications', 'PhotoLibrary', - 'PhotoLibraryAddOnly', + # 'PhotoLibraryAddOnly', # 'Reminders', # 'Siri', # 'SpeechRecognition', diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b69bf34d1..03d48408e 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1477,12 +1477,12 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: d3f49c53809116a5d38da093a8aa78bf551aed09 BVLinearGradient: 880f91a7854faff2df62518f0281afb1c60d49a3 - DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 + DoubleConversion: fea03f2699887d960129cc54bba7e52542b6f953 FasterImage: 60d0750ddbcefff0070c4c17309c2d1d6cc650f0 FBLazyVector: 9f533d5a4c75ca77c8ed774aced1a91a0701781e FBReactNativeSpec: 40b791f4a1df779e7e4aa12c000319f4f216d40a fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b + glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 hermes-engine: 39589e9c297d024e90fe68f6830ff86c4e01498a libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 MMKV: 36a22a9ec84c9bb960613a089ddf6f48be9312b0 @@ -1560,7 +1560,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: bc2cdb2dc42facdf34992ae364b8a728e19a3686 RNLocalize: e8694475db034bf601e17bd3dfa8986565e769eb - RNPermissions: b3d6efca086546e29a2920cd649a0ab04ca77794 + RNPermissions: a123c47480a5f5d7a04d40637ad1f7360a41b465 RNReanimated: 6cfa556540186ce7ae7a0b048f369236b1d86ebb RNScreens: b6b64d956af3715adbfe84808694ae82d3fec74f RNShareMenu: cb9dac548c8bf147d06f0bf07296ad51ea9f5fc3 @@ -1572,6 +1572,6 @@ SPEC CHECKSUMS: VisionCameraPluginInatVision: 8480b3955bc608e913135d3bebaa57939911fb82 Yoga: c716aea2ee01df6258550c7505fa61b248145ced -PODFILE CHECKSUM: eebd76aa39f99b44754431ed68ce0cfbfc5ec2f7 +PODFILE CHECKSUM: ebb6b37cf92e00a96e3123d4db14c5658b4e5929 COCOAPODS: 1.15.2 diff --git a/ios/iNaturalistReactNative/Info.plist b/ios/iNaturalistReactNative/Info.plist index 3de8a7977..00b9eea28 100644 --- a/ios/iNaturalistReactNative/Info.plist +++ b/ios/iNaturalistReactNative/Info.plist @@ -54,7 +54,7 @@ NSAppleMusicUsageDescription Add existing photos and sounds to your observations. NSCameraUsageDescription - ${PRODUCT_NAME} uses the camera to add photos to your observations of nature. + iNaturalist Next uses the camera to add photos to your observations of nature. NSLocationAlwaysAndWhenInUseUsageDescription We do not intentionally request this permission. If you are seeing this, please take a screenshot and email it to help@inaturalist.org NSLocationWhenInUseUsageDescription @@ -62,9 +62,9 @@ NSMicrophoneUsageDescription Record sound observations of nature. NSPhotoLibraryAddUsageDescription - Export ${PRODUCT_NAME} photos to your library. + Export iNaturalist Next photos to your library. NSPhotoLibraryUsageDescription - Export and import ${PRODUCT_NAME} photos to and from your library. + Export and import iNaturalist Next photos to and from your library. UIAppFonts Lato-Bold.ttf diff --git a/src/components/Camera/AICamera/AICamera.js b/src/components/Camera/AICamera/AICamera.js index e18789211..0549e927f 100644 --- a/src/components/Camera/AICamera/AICamera.js +++ b/src/components/Camera/AICamera/AICamera.js @@ -42,7 +42,7 @@ type Props = { camera: Object, device: Object, flipCamera: Function, - handleCheckmarkPress: Function, + handleCheckmarkPress: ( _result: any | null ) => void, isLandscapeMode: boolean }; diff --git a/src/components/Camera/AICamera/AICameraButtons.js b/src/components/Camera/AICamera/AICameraButtons.js index c55d86825..f87e138cb 100644 --- a/src/components/Camera/AICamera/AICameraButtons.js +++ b/src/components/Camera/AICamera/AICameraButtons.js @@ -3,7 +3,7 @@ import CameraFlip from "components/Camera/Buttons/CameraFlip"; import Close from "components/Camera/Buttons/Close"; import Flash from "components/Camera/Buttons/Flash"; -import TakePhoto from "components/Camera/Buttons/TakePhoto"; +import TakePhoto from "components/Camera/Buttons/TakePhoto.tsx"; import Zoom from "components/Camera/Buttons/Zoom"; import TabletButtons from "components/Camera/TabletButtons"; import { View } from "components/styledComponents"; @@ -60,7 +60,7 @@ type Props = { setNumStoredResults?: Function, showPrediction: boolean, showZoomButton: boolean, - takePhoto: Function, + takePhoto: () => Promise, takePhotoOptions: Object, toggleFlash: Function, zoomTextValue: string diff --git a/src/components/Camera/Buttons/TakePhoto.js b/src/components/Camera/Buttons/TakePhoto.tsx similarity index 91% rename from src/components/Camera/Buttons/TakePhoto.js rename to src/components/Camera/Buttons/TakePhoto.tsx index f8d08468a..6eaf06090 100644 --- a/src/components/Camera/Buttons/TakePhoto.js +++ b/src/components/Camera/Buttons/TakePhoto.tsx @@ -1,5 +1,3 @@ -// @flow - import classnames from "classnames"; import { INatIcon @@ -7,7 +5,6 @@ import { import { Pressable, View } from "components/styledComponents"; -import type { Node } from "react"; import React from "react"; import { useTheme } from "react-native-paper"; import { useTranslation } from "sharedHooks"; @@ -16,17 +13,17 @@ import colors from "styles/tailwindColors"; const DROP_SHADOW = getShadowForColor( colors.darkGray ); -type Props = { - takePhoto: Function, - disabled: boolean, - showPrediction?: boolean +interface Props { + takePhoto: () => Promise; + disabled: boolean; + showPrediction?: boolean; } const TakePhoto = ( { takePhoto, disabled, showPrediction -}: Props ): Node => { +}: Props ) => { const { t } = useTranslation( ); const theme = useTheme( ); diff --git a/src/components/Camera/CameraWithDevice.tsx b/src/components/Camera/CameraWithDevice.tsx index cac1655a1..7b0c0b0cd 100644 --- a/src/components/Camera/CameraWithDevice.tsx +++ b/src/components/Camera/CameraWithDevice.tsx @@ -1,11 +1,7 @@ import { useIsFocused, useNavigation } from "@react-navigation/native"; -import LocationPermissionGate from "components/SharedComponents/LocationPermissionGate"; import PermissionGateContainer, { - LOCATION_PERMISSIONS, - permissionResultFromMultiple, - WRITE_MEDIA_PERMISSIONS -} - from "components/SharedComponents/PermissionGateContainer"; + READ_WRITE_MEDIA_PERMISSIONS +} from "components/SharedComponents/PermissionGateContainer.tsx"; import { View } from "components/styledComponents"; import React, { useCallback, useEffect, useRef, useState @@ -13,11 +9,6 @@ import React, { import { StatusBar } from "react-native"; import DeviceInfo from "react-native-device-info"; import Orientation from "react-native-orientation-locker"; -import { - checkMultiple, - Permission, - RESULTS -} from "react-native-permissions"; import { Camera, CameraDevice } from "react-native-vision-camera"; // import { log } from "sharedHelpers/logger"; import { useTranslation } from "sharedHooks"; @@ -25,6 +16,7 @@ import useDeviceOrientation, { LANDSCAPE_LEFT, LANDSCAPE_RIGHT } from "sharedHooks/useDeviceOrientation.ts"; +import useLocationPermission from "sharedHooks/useLocationPermission.tsx"; import AICamera from "./AICamera/AICamera"; import usePrepareStoreAndNavigate from "./hooks/usePrepareStoreAndNavigate"; @@ -69,7 +61,16 @@ const CameraWithDevice = ( { // permission gate on ObsEdit const [addPhotoPermissionGateWasClosed, setAddPhotoPermissionGateWasClosed] = useState( false ); const isFocused = useIsFocused( ); - const [locationPermissionGranted, setLocationPermissionGranted] = useState( false ); + + // Check if location permission granted b/c usePrepareStoreAndNavigate and + // useUserLocation need to know if permission has been granted to fetch the + // user's location while the camera is active. We don't want to *ask* for + // permission here b/c we want to avoid overloading a new user with + // permission requests and they will just have seen the camera permission + // request before landing here, so it's ok if we're not fetching the + // location here for the user's first observation (suggestions might be a + // bit off and we'll fetch the obs coordinates on ObsEdit) + const { hasPermissions } = useLocationPermission( ); // logger.debug( `isFocused: ${isFocused}` ); const prepareStoreAndNavigate = usePrepareStoreAndNavigate( { @@ -80,7 +81,7 @@ const CameraWithDevice = ( { // up and use that to pass along to Suggestions when the user navigates // there... but we only want to do that while the camera has focus and we // have permission - shouldFetchLocation: isFocused && locationPermissionGranted + shouldFetchLocation: isFocused && !!hasPermissions } ); const isLandscapeMode = [LANDSCAPE_LEFT, LANDSCAPE_RIGHT].includes( deviceOrientation ); @@ -154,66 +155,25 @@ const CameraWithDevice = ( { return unsubscribe; }, [navigation] ); - // Check if location permission granted b/c usePrepareStoreAndNavigate and - // useUserLocation need to know if permission has been granted to fetch the - // user's location while the camera is active. We don't want to *ask* for - // permission here b/c we want to avoid overloading a new user with - // permission requests and they will just have seen the camera permission - // request before landing here, so it's ok if we're not fetching the - // location here for the user's first observation (suggestions might be a - // bit off and we'll fetch the obs coordinates on ObsEdit) - useEffect( ( ) => { - async function checkLocationPermissions() { - const permissionsResult = permissionResultFromMultiple( - await checkMultiple( LOCATION_PERMISSIONS as Permission[] ) - ); - if ( permissionsResult === RESULTS.GRANTED ) { - setLocationPermissionGranted( true ); - } else { - console.warn( - "Location permissions have not been granted. You probably need to use a PermissionGate" - ); - } - } - checkLocationPermissions( ); - }, [] ); - return ( - {/* TODO why is this even here? The camera doesn't need location - permissions. Suggestions does. ~~~~kueda20240611 */} - {/* a weird quirk of react-native-modal is you can show subsequent modals - when a modal is nested in another modal. location permission is shown first - because the save photo modal pops up a second system alert on iOS asking - how much access to give */} - setAddPhotoPermissionGateWasClosed( true )} + onPermissionGranted={onPhotoPermissionGranted} + onPermissionDenied={onPhotoPermissionDenied} withoutNavigation - onPermissionGranted={( ) => { - // This probably doesn't do anything, but on the off chance we're - // able to grab coordinates immediately after the user grants - // permission, that will probably yield better suggestions on the - // next screen than nothing. - setLocationPermissionGranted( true ); - }} - > - setAddPhotoPermissionGateWasClosed( true )} - onPermissionGranted={onPhotoPermissionGranted} - onPermissionDenied={onPhotoPermissionDenied} - withoutNavigation - permissionNeeded={checkmarkTapped} - /> - + permissionNeeded={checkmarkTapped} + /> {cameraType === "Standard" ? ( Promise, } const CameraNavButtons = ( { diff --git a/src/components/Camera/StandardCamera/CameraOptionsButtons.js b/src/components/Camera/StandardCamera/CameraOptionsButtons.js index 0355cabd3..7fcc23f06 100644 --- a/src/components/Camera/StandardCamera/CameraOptionsButtons.js +++ b/src/components/Camera/StandardCamera/CameraOptionsButtons.js @@ -13,7 +13,7 @@ import Animated from "react-native-reanimated"; const isTablet = DeviceInfo.isTablet(); type Props = { - takePhoto: Function, + takePhoto: () => Promise, handleClose: Function, disabled: boolean, photosTaken: boolean, diff --git a/src/components/Camera/TabletButtons.js b/src/components/Camera/TabletButtons.js index 960444254..95d773338 100644 --- a/src/components/Camera/TabletButtons.js +++ b/src/components/Camera/TabletButtons.js @@ -47,7 +47,7 @@ type Props = { rotatableAnimatedStyle: Object, showPrediction?: boolean, showZoomButton: boolean, - takePhoto: Function, + takePhoto: () => Promise, takePhotoOptions: Object, toggleFlash: Function, zoomTextValue: string diff --git a/src/components/Explore/Explore.js b/src/components/Explore/Explore.js index b7f2bd919..ae4a9bd80 100644 --- a/src/components/Explore/Explore.js +++ b/src/components/Explore/Explore.js @@ -4,12 +4,15 @@ import { refresh } from "@react-native-community/netinfo"; import classnames from "classnames"; import ExploreFiltersModal from "components/Explore/Modals/ExploreFiltersModal"; import { + Body2, + Button, INatIconButton, OfflineNotice, RadioButtonSheet, ViewWrapper } from "components/SharedComponents"; import { View } from "components/styledComponents"; +import { PLACE_MODE } from "providers/ExploreContext.tsx"; import type { Node } from "react"; import React, { useState } from "react"; import { Alert } from "react-native"; @@ -56,7 +59,11 @@ type Props = { updateTaxon: Function, updateLocation: Function, updateUser: Function, - updateProject: Function + updateProject: Function, + // TODO: change to PLACE_MODE in Typescript + placeMode: string, + hasLocationPermissions: ?boolean, + requestLocationPermissions: Function } const Explore = ( { @@ -73,7 +80,10 @@ const Explore = ( { updateTaxon, updateLocation, updateUser, - updateProject + updateProject, + placeMode, + hasLocationPermissions, + requestLocationPermissions }: Props ): Node => { const theme = useTheme( ); const { t } = useTranslation( ); @@ -107,6 +117,69 @@ const Explore = ( { /> ); + const renderMainContent = ( ) => { + if ( !isOnline ) { + return ( + refresh()} + /> + ); + } + // hasLocationPermissions === undefined means we haven't checked for location permissions yet + if ( placeMode === PLACE_MODE.NEARBY && hasLocationPermissions === false ) { + return ( + + + {t( "To-view-nearby-organisms-please-enable-location" )} + +