mirror of
https://github.com/inaturalist/iNaturalistReactNative.git
synced 2025-12-23 22:18:36 -05:00
Handle merge conflicts from main
This commit is contained in:
@@ -111,8 +111,8 @@ android {
|
||||
applicationId "org.inaturalist.iNaturalistMobile"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 156
|
||||
versionName "0.59.13"
|
||||
versionCode 157
|
||||
versionName "0.59.14"
|
||||
setProperty("archivesBaseName", applicationId + "-v" + versionName + "+" + versionCode)
|
||||
manifestPlaceholders = [ GMAPS_API_KEY:project.env.get("GMAPS_API_KEY") ]
|
||||
// Detox Android setup
|
||||
|
||||
1
fastlane/metadata/android/en-US/changelogs/157.txt
Normal file
1
fastlane/metadata/android/en-US/changelogs/157.txt
Normal file
@@ -0,0 +1 @@
|
||||
Bug fixes for location fetching while creating an observation and for bulk observation uploads
|
||||
@@ -646,7 +646,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = iNaturalistReactNative/iNaturalistReactNative.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CURRENT_PROJECT_VERSION = 156;
|
||||
CURRENT_PROJECT_VERSION = 157;
|
||||
DEVELOPMENT_TEAM = N5J7L4P93Z;
|
||||
ENABLE_BITCODE = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
@@ -773,7 +773,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = iNaturalistReactNative/iNaturalistReactNativeRelease.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
|
||||
CURRENT_PROJECT_VERSION = 156;
|
||||
CURRENT_PROJECT_VERSION = 157;
|
||||
DEVELOPMENT_TEAM = N5J7L4P93Z;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
|
||||
HEADER_SEARCH_PATHS = (
|
||||
@@ -1053,7 +1053,7 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = "iNaturalistReactNative-ShareExtension/iNaturalistReactNative-ShareExtension.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 156;
|
||||
CURRENT_PROJECT_VERSION = 157;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = N5J7L4P93Z;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64";
|
||||
@@ -1098,7 +1098,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 156;
|
||||
CURRENT_PROJECT_VERSION = 157;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = N5J7L4P93Z;
|
||||
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64";
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.59.13</string>
|
||||
<string>0.59.14</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@@ -40,7 +40,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>156</string>
|
||||
<string>157</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.59.13</string>
|
||||
<string>0.59.14</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>156</string>
|
||||
<string>157</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "inaturalistreactnative",
|
||||
"version": "0.59.13",
|
||||
"version": "0.59.14",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "inaturalistreactnative",
|
||||
"version": "0.59.13",
|
||||
"version": "0.59.14",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.11",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "inaturalistreactnative",
|
||||
"version": "0.59.13",
|
||||
"version": "0.59.14",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
|
||||
@@ -13,9 +13,10 @@ import { Alert, StatusBar } from "react-native";
|
||||
import type {
|
||||
TakePhotoOptions
|
||||
} from "react-native-vision-camera";
|
||||
import fetchAccurateUserLocation from "sharedHelpers/fetchAccurateUserLocation.ts";
|
||||
import { createSentinelFile, deleteSentinelFile, logStage } from "sharedHelpers/sentinelFiles.ts";
|
||||
import {
|
||||
useDeviceOrientation, useTranslation, useWatchPosition
|
||||
useDeviceOrientation, useTranslation
|
||||
} from "sharedHooks";
|
||||
import useLocationPermission from "sharedHooks/useLocationPermission.tsx";
|
||||
import useStore from "stores/useStore";
|
||||
@@ -77,22 +78,17 @@ const CameraContainer = ( ) => {
|
||||
}, [cameraType, sentinelFileName] );
|
||||
|
||||
const { deviceOrientation } = useDeviceOrientation( );
|
||||
// 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: hasLocationPermissions,
|
||||
renderPermissionsGate: renderLocationPermissionsGate,
|
||||
requestPermissions: requestLocationPermissions
|
||||
} = useLocationPermission( );
|
||||
const { userLocation } = useWatchPosition( {
|
||||
shouldFetchLocation: !!( hasLocationPermissions )
|
||||
} );
|
||||
// we don't want to use this for the observation location because
|
||||
// a user could be walking with the camera open for a while, so this location
|
||||
// will not reflect when they actually took the photo
|
||||
const [userLocationForGeomodel, setUserLocationForGeomodel] = useState( null );
|
||||
|
||||
const navigation = useNavigation( );
|
||||
const { t } = useTranslation( );
|
||||
|
||||
@@ -123,13 +119,10 @@ const CameraContainer = ( ) => {
|
||||
const generateSentinelFile = async ( ) => {
|
||||
const fileName = await createSentinelFile( "AICamera" );
|
||||
setSentinelFileName( fileName );
|
||||
if ( hasLocationPermissions ) {
|
||||
await logStage( fileName, "fetch_user_location_start" );
|
||||
}
|
||||
};
|
||||
if ( cameraType !== "AI" ) { return; }
|
||||
generateSentinelFile( );
|
||||
}, [setSentinelFileName, cameraType, hasLocationPermissions] );
|
||||
}, [setSentinelFileName, cameraType] );
|
||||
|
||||
const {
|
||||
hasPermissions: hasSavePhotoPermission,
|
||||
@@ -152,9 +145,8 @@ const CameraContainer = ( ) => {
|
||||
};
|
||||
|
||||
const navigationOptions = useMemo( ( ) => ( {
|
||||
addPhotoPermissionResult,
|
||||
userLocation
|
||||
} ), [addPhotoPermissionResult, userLocation] );
|
||||
addPhotoPermissionResult
|
||||
} ), [addPhotoPermissionResult] );
|
||||
|
||||
const prepareStoreAndNavigate = usePrepareStoreAndNavigate( );
|
||||
|
||||
@@ -165,8 +157,15 @@ const CameraContainer = ( ) => {
|
||||
newPhotoState: PhotoState,
|
||||
visionResult: StoredResult | null
|
||||
) => {
|
||||
// fetch accurate user location, with a fallback to a course location
|
||||
// at the time the user taps AI shutter or multicapture checkmark
|
||||
// to create an observation
|
||||
// this handles checking for location, and we do *not* want to show
|
||||
// location permissions in the camera, so we no longer need to check for that
|
||||
const accurateUserLocation = await fetchAccurateUserLocation( );
|
||||
await prepareStoreAndNavigate( {
|
||||
...navigationOptions,
|
||||
userLocation: accurateUserLocation,
|
||||
newPhotoState,
|
||||
logStageIfAICamera,
|
||||
deleteStageIfAICamera,
|
||||
@@ -258,6 +257,18 @@ const CameraContainer = ( ) => {
|
||||
return uri;
|
||||
};
|
||||
|
||||
useEffect( ( ) => {
|
||||
const fetchLocation = async ( ) => {
|
||||
const accurateUserLocation = await fetchAccurateUserLocation( );
|
||||
setUserLocationForGeomodel( accurateUserLocation );
|
||||
return accurateUserLocation;
|
||||
};
|
||||
|
||||
if ( hasLocationPermissions ) {
|
||||
fetchLocation( );
|
||||
}
|
||||
}, [hasLocationPermissions] );
|
||||
|
||||
if ( !device ) {
|
||||
Alert.alert(
|
||||
t( "No-Camera-Available" ),
|
||||
@@ -282,13 +293,18 @@ const CameraContainer = ( ) => {
|
||||
takePhotoOptions={takePhotoOptions}
|
||||
newPhotoUris={newPhotoUris}
|
||||
setNewPhotoUris={setNewPhotoUris}
|
||||
userLocation={userLocation}
|
||||
userLocation={userLocationForGeomodel}
|
||||
hasLocationPermissions={hasLocationPermissions}
|
||||
requestLocationPermissions={requestLocationPermissions}
|
||||
/>
|
||||
{showPhotoPermissionsGate && renderSavePhotoPermissionGate( {
|
||||
onPermissionGranted: async ( ) => {
|
||||
const savedPhotoUris = await savePhotosToPhotoLibrary( cameraUris, userLocation );
|
||||
// we need this to make sure the very first photo after permission granted
|
||||
// is saved to device. very unlikely that we'll have a location here
|
||||
// since we're not prompting for permission, but there are a few scenarios where it
|
||||
// could happen, like a user enabling location on Explore before visiting the camera
|
||||
const accurateUserLocation = await fetchAccurateUserLocation( );
|
||||
const savedPhotoUris = await savePhotosToPhotoLibrary( cameraUris, accurateUserLocation );
|
||||
await logStageIfAICamera( "save_photos_to_photo_library_first_permission" );
|
||||
if ( savedPhotoUris.length > 0 ) {
|
||||
// Save these camera roll URIs, so later on observation editor can update
|
||||
|
||||
@@ -186,7 +186,7 @@ const GroupPhotos = ( {
|
||||
</FloatingActionBar>
|
||||
<ButtonBar
|
||||
sticky
|
||||
containerClass="items-center z-50"
|
||||
containerClass="items-center z-50 bg-white"
|
||||
onLayout={onLayout}
|
||||
>
|
||||
<Button
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
useTranslation
|
||||
} from "sharedHooks";
|
||||
|
||||
import fetchUserLocation from "../../sharedHelpers/fetchUserLocation";
|
||||
import fetchCoarseUserLocation from "../../sharedHelpers/fetchCoarseUserLocation";
|
||||
import useInfiniteProjectsScroll from "./hooks/useInfiniteProjectsScroll";
|
||||
import Projects from "./Projects";
|
||||
|
||||
@@ -48,7 +48,7 @@ const ProjectsContainer = ( ) => {
|
||||
}
|
||||
|
||||
const getCurrentUserLocation = async ( ) => {
|
||||
const currentUserLocation = await fetchUserLocation( );
|
||||
const currentUserLocation = await fetchCoarseUserLocation( );
|
||||
setUserLocation( currentUserLocation );
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import MapView, {
|
||||
BoundingBox, LatLng, MapType, Region, UrlTile
|
||||
} from "react-native-maps";
|
||||
import Observation from "realmModels/Observation";
|
||||
import fetchUserLocation from "sharedHelpers/fetchUserLocation.ts";
|
||||
import fetchCoarseUserLocation from "sharedHelpers/fetchCoarseUserLocation.ts";
|
||||
import { useDebugMode, useDeviceOrientation } from "sharedHooks";
|
||||
import useLocationPermission from "sharedHooks/useLocationPermission.tsx";
|
||||
import colors from "styles/tailwindColors";
|
||||
@@ -203,7 +203,7 @@ const Map = ( {
|
||||
}, [params, currentZoom, navigation] );
|
||||
|
||||
const onPermissionGranted = async ( ) => {
|
||||
const currentLocation = await fetchUserLocation( );
|
||||
const currentLocation = await fetchCoarseUserLocation( );
|
||||
if ( currentLocation && mapViewRef?.current ) {
|
||||
animateToRegion( {
|
||||
latitude: currentLocation.latitude,
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import { isDebugMode } from "sharedHooks/useDebugMode";
|
||||
import useStore from "stores/useStore";
|
||||
|
||||
import fetchCoarseUserLocation from "../../sharedHelpers/fetchUserLocation";
|
||||
import fetchCoarseUserLocation from "../../sharedHelpers/fetchCoarseUserLocation";
|
||||
import flattenUploadParams from "./helpers/flattenUploadParams";
|
||||
import useClearComputerVisionDirectory from "./hooks/useClearComputerVisionDirectory";
|
||||
import useNavigateWithTaxonSelected from "./hooks/useNavigateWithTaxonSelected";
|
||||
|
||||
@@ -5,7 +5,7 @@ import Taxon from "realmModels/Taxon";
|
||||
import safeRealmWrite from "sharedHelpers/safeRealmWrite";
|
||||
import { useAuthenticatedQuery, useLocationPermission } from "sharedHooks";
|
||||
|
||||
import fetchUserLocation from "../../sharedHelpers/fetchUserLocation";
|
||||
import fetchCoarseUserLocation from "../../sharedHelpers/fetchCoarseUserLocation";
|
||||
|
||||
const { useRealm } = RealmContext;
|
||||
|
||||
@@ -54,7 +54,7 @@ const useTaxonCommonNames = ( ) => {
|
||||
|
||||
useEffect( ( ) => {
|
||||
const fetchLocation = async ( ) => {
|
||||
const location = await fetchUserLocation( );
|
||||
const location = await fetchCoarseUserLocation( );
|
||||
setUserLocation( location );
|
||||
};
|
||||
if ( hasPermissions ) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { LatLng } from "react-native-maps";
|
||||
|
||||
// Please don't change this to an aliased path or the e2e mock will not get
|
||||
// used in our e2e tests on Github Actions
|
||||
import fetchCoarseUserLocation from "../sharedHelpers/fetchUserLocation";
|
||||
import fetchCoarseUserLocation from "../sharedHelpers/fetchCoarseUserLocation";
|
||||
|
||||
export enum EXPLORE_ACTION {
|
||||
CHANGE_SORT_BY = "CHANGE_SORT_BY",
|
||||
|
||||
57
src/sharedHelpers/fetchAccurateUserLocation.ts
Normal file
57
src/sharedHelpers/fetchAccurateUserLocation.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// Please don't change this to an aliased path or the e2e mock will not get
|
||||
// used in our e2e tests on Github Actions
|
||||
import {
|
||||
checkLocationPermissions,
|
||||
getCurrentPositionWithOptions,
|
||||
highAccuracyOptions,
|
||||
lowAccuracyOptions
|
||||
} from "./geolocationWrapper";
|
||||
|
||||
interface UserLocation {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
positional_accuracy: number;
|
||||
altitude: number | null;
|
||||
altitudinal_accuracy: number | null;
|
||||
}
|
||||
|
||||
const fetchAccurateUserLocation = async (): Promise<UserLocation | null> => {
|
||||
const permissionResult = await checkLocationPermissions( );
|
||||
if ( permissionResult === null ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const highAccuracyResult = await getCurrentPositionWithOptions( highAccuracyOptions )
|
||||
.catch( error => {
|
||||
console.warn( "High accuracy location failed, falling back to low accuracy", error );
|
||||
return null;
|
||||
} );
|
||||
|
||||
if ( highAccuracyResult ) {
|
||||
return {
|
||||
latitude: highAccuracyResult.coords.latitude,
|
||||
longitude: highAccuracyResult.coords.longitude,
|
||||
positional_accuracy: highAccuracyResult.coords.accuracy,
|
||||
altitude: highAccuracyResult.coords.altitude,
|
||||
altitudinal_accuracy: highAccuracyResult.coords.altitudeAccuracy
|
||||
};
|
||||
}
|
||||
|
||||
const lowAccuracyResult = await getCurrentPositionWithOptions( lowAccuracyOptions, 2 );
|
||||
|
||||
return {
|
||||
latitude: lowAccuracyResult.coords.latitude,
|
||||
longitude: lowAccuracyResult.coords.longitude,
|
||||
positional_accuracy: lowAccuracyResult.coords.accuracy,
|
||||
altitude: lowAccuracyResult.coords.altitude,
|
||||
altitudinal_accuracy: lowAccuracyResult.coords.altitudeAccuracy
|
||||
};
|
||||
} catch ( e ) {
|
||||
console.warn( "All location attempts failed:", e );
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default fetchAccurateUserLocation;
|
||||
39
src/sharedHelpers/fetchCoarseUserLocation.ts
Normal file
39
src/sharedHelpers/fetchCoarseUserLocation.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// Please don't change this to an aliased path or the e2e mock will not get
|
||||
// used in our e2e tests on Github Actions
|
||||
import {
|
||||
checkLocationPermissions,
|
||||
getCurrentPositionWithOptions,
|
||||
lowAccuracyOptions
|
||||
} from "./geolocationWrapper";
|
||||
|
||||
interface UserLocation {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
positional_accuracy: number;
|
||||
altitude: number | null;
|
||||
altitudinal_accuracy: number | null;
|
||||
}
|
||||
|
||||
const fetchCoarseUserLocation = async ( ): Promise<UserLocation | null> => {
|
||||
const permissionResult = await checkLocationPermissions( );
|
||||
if ( permissionResult === null ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const { coords } = await getCurrentPositionWithOptions( lowAccuracyOptions );
|
||||
const userLocation = {
|
||||
latitude: coords.latitude,
|
||||
longitude: coords.longitude,
|
||||
positional_accuracy: coords.accuracy,
|
||||
altitude: coords.altitude,
|
||||
altitudinal_accuracy: coords.altitudeAccuracy
|
||||
};
|
||||
return userLocation;
|
||||
} catch ( e ) {
|
||||
console.warn( e, "couldn't get latLng" );
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default fetchCoarseUserLocation;
|
||||
@@ -1,23 +0,0 @@
|
||||
type UserLocation = {
|
||||
latitude: number,
|
||||
longitude: number,
|
||||
positional_accuracy: number
|
||||
|
||||
}
|
||||
const fetchUserLocation = async ( ): Promise<UserLocation> => new Promise( resolve => {
|
||||
setTimeout( ( ) => resolve( {
|
||||
// Darwin's house. Note that the e2e tests run in a UTC environment, so the
|
||||
// observed_on_string will be set to a UTC time. If these coordinates
|
||||
// fall within a time zone west of that (i.e. in the past),
|
||||
// observation creation will fail during the period of the day when UTC
|
||||
// time has crossed into the date after the date at these coordinates.
|
||||
// The opposite (when local time is in a time zone behind the time zone of
|
||||
// the coordinates) will not fail, but it might make an observation
|
||||
// that's a day behind when you were expecting.
|
||||
latitude: 51.3313127,
|
||||
longitude: 0.0509862,
|
||||
positional_accuracy: 5
|
||||
} ), 1000 );
|
||||
} );
|
||||
|
||||
export default fetchUserLocation;
|
||||
@@ -1,65 +0,0 @@
|
||||
import Geolocation, { GeolocationResponse } from "@react-native-community/geolocation";
|
||||
import {
|
||||
LOCATION_PERMISSIONS,
|
||||
permissionResultFromMultiple
|
||||
} from "components/SharedComponents/PermissionGateContainer.tsx";
|
||||
import { Platform } from "react-native";
|
||||
import {
|
||||
checkMultiple,
|
||||
RESULTS
|
||||
} from "react-native-permissions";
|
||||
|
||||
const options = {
|
||||
enableHighAccuracy: false,
|
||||
timeout: 2000,
|
||||
// Setting maximumAge to 0 always causes errors on Android.
|
||||
// Therefore, we conditionally apply it only if the platform is iOS.
|
||||
...( Platform.OS === "ios" && { maximumAge: 0 } )
|
||||
} as const;
|
||||
|
||||
// Issue reference for getCurrentPosition bug on Android:
|
||||
// Known bug in react-native-geolocation: getCurrentPosition does not work on
|
||||
// Android when enableHighAccuracy: true and maximumAge: 0.
|
||||
// See: https://github.com/michalchudziak/react-native-geolocation/issues/272
|
||||
// Added OS-specific conditions to handle this issue and make it work properly on Android.
|
||||
const getCurrentPosition = ( ): Promise<GeolocationResponse> => new Promise(
|
||||
( resolve, error ) => {
|
||||
Geolocation.getCurrentPosition( resolve, error, options );
|
||||
}
|
||||
);
|
||||
|
||||
interface UserLocation {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
positional_accuracy: number;
|
||||
altitude: number | null;
|
||||
altitudinal_accuracy: number | null;
|
||||
}
|
||||
|
||||
const fetchCoarseUserLocation = async ( ): Promise<UserLocation | null> => {
|
||||
const permissionResult = permissionResultFromMultiple(
|
||||
await checkMultiple( LOCATION_PERMISSIONS )
|
||||
);
|
||||
|
||||
// TODO: handle case where iOS permissions are not granted
|
||||
if ( Platform.OS !== "android" && permissionResult !== RESULTS.GRANTED ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const { coords } = await getCurrentPosition( );
|
||||
const userLocation = {
|
||||
latitude: coords.latitude,
|
||||
longitude: coords.longitude,
|
||||
positional_accuracy: coords.accuracy,
|
||||
altitude: coords.altitude,
|
||||
altitudinal_accuracy: coords.altitudeAccuracy
|
||||
};
|
||||
return userLocation;
|
||||
} catch ( e ) {
|
||||
console.warn( e, "couldn't get latLng" );
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default fetchCoarseUserLocation;
|
||||
@@ -2,9 +2,33 @@ import {
|
||||
GeolocationResponse
|
||||
} from "@react-native-community/geolocation";
|
||||
import { CHUCKS_PAD } from "appConstants/e2e.ts";
|
||||
import {
|
||||
LOCATION_PERMISSIONS,
|
||||
permissionResultFromMultiple
|
||||
} from "components/SharedComponents/PermissionGateContainer.tsx";
|
||||
import { Platform } from "react-native";
|
||||
import {
|
||||
checkMultiple,
|
||||
RESULTS
|
||||
} from "react-native-permissions";
|
||||
|
||||
let counter = 0;
|
||||
|
||||
function getCurrentPosition(
|
||||
success: (position: GeolocationResponse) => void,
|
||||
error?: (error: any) => void,
|
||||
options?: any
|
||||
) {
|
||||
console.log("[DEBUG geolocationWrapper.e2e-mock] getCurrentPosition");
|
||||
setTimeout(() => {
|
||||
console.log("[DEBUG geolocationWrapper.e2e-mock] getCurrentPosition success");
|
||||
success({
|
||||
coords: CHUCKS_PAD,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function watchPosition(
|
||||
success: ( position: GeolocationResponse ) => void
|
||||
) {
|
||||
@@ -31,8 +55,44 @@ function clearWatch( watchID: number ) {
|
||||
console.log( "[DEBUG geolocationWrapper.e2e-mock] clearWatch, watchID: ", watchID );
|
||||
}
|
||||
|
||||
export {
|
||||
CHUCKS_PAD,
|
||||
clearWatch,
|
||||
watchPosition
|
||||
const highAccuracyOptions = {
|
||||
enableHighAccuracy: true,
|
||||
timeout: 10000,
|
||||
...( Platform.OS === "ios" && { maximumAge: 0 } )
|
||||
};
|
||||
|
||||
const lowAccuracyOptions = {
|
||||
enableHighAccuracy: false,
|
||||
timeout: 2000,
|
||||
...( Platform.OS === "ios" && { maximumAge: 0 } )
|
||||
};
|
||||
|
||||
const getCurrentPositionWithOptions = (
|
||||
options
|
||||
): Promise<GeolocationResponse> => new Promise(
|
||||
( resolve, reject ) => {
|
||||
console.log("[DEBUG geolocationWrapper.e2e-mock] getCurrentPositionWithOptions");
|
||||
getCurrentPosition( resolve, reject, options );
|
||||
}
|
||||
);
|
||||
|
||||
const checkLocationPermissions = async ( ) => {
|
||||
const permissionResult = permissionResultFromMultiple(
|
||||
await checkMultiple( LOCATION_PERMISSIONS )
|
||||
);
|
||||
console.log("[DEBUG geolocationWrapper.e2e-mock] checkLocationPermissions");
|
||||
|
||||
if ( Platform.OS !== "android" && permissionResult !== RESULTS.GRANTED ) {
|
||||
return null;
|
||||
}
|
||||
return permissionResult;
|
||||
};
|
||||
|
||||
export {
|
||||
clearWatch,
|
||||
watchPosition,
|
||||
highAccuracyOptions,
|
||||
lowAccuracyOptions,
|
||||
getCurrentPositionWithOptions,
|
||||
checkLocationPermissions
|
||||
};
|
||||
|
||||
@@ -5,6 +5,27 @@ import Geolocation, {
|
||||
GeolocationError,
|
||||
GeolocationResponse
|
||||
} from "@react-native-community/geolocation";
|
||||
import {
|
||||
LOCATION_PERMISSIONS,
|
||||
permissionResultFromMultiple
|
||||
} from "components/SharedComponents/PermissionGateContainer.tsx";
|
||||
import { Platform } from "react-native";
|
||||
import {
|
||||
checkMultiple,
|
||||
RESULTS
|
||||
} from "react-native-permissions";
|
||||
|
||||
export function getCurrentPosition(
|
||||
success: ( position: GeolocationResponse ) => void,
|
||||
error?: ( error: GeolocationError ) => void,
|
||||
options?: {
|
||||
timeout?: number;
|
||||
maximumAge?: number;
|
||||
enableHighAccuracy?: boolean;
|
||||
}
|
||||
) {
|
||||
return Geolocation.getCurrentPosition( success, error, options );
|
||||
}
|
||||
|
||||
export function watchPosition(
|
||||
success: ( position: GeolocationResponse ) => void,
|
||||
@@ -25,3 +46,41 @@ export function watchPosition(
|
||||
export function clearWatch( watchID: number ) {
|
||||
Geolocation.clearWatch( watchID );
|
||||
}
|
||||
|
||||
// Issue reference for getCurrentPosition bug on Android:
|
||||
// Known bug in react-native-geolocation: getCurrentPosition does not work on
|
||||
// Android when enableHighAccuracy: true and maximumAge: 0.
|
||||
// See: https://github.com/michalchudziak/react-native-geolocation/issues/272
|
||||
// Added OS-specific conditions to both options below
|
||||
// to handle this issue and make it work properly on Android.
|
||||
export const highAccuracyOptions = {
|
||||
enableHighAccuracy: true,
|
||||
timeout: 10000,
|
||||
...( Platform.OS === "ios" && { maximumAge: 0 } )
|
||||
} as const;
|
||||
|
||||
export const lowAccuracyOptions = {
|
||||
enableHighAccuracy: false,
|
||||
timeout: 2000,
|
||||
...( Platform.OS === "ios" && { maximumAge: 0 } )
|
||||
} as const;
|
||||
|
||||
export const getCurrentPositionWithOptions = (
|
||||
options
|
||||
): Promise<GeolocationResponse> => new Promise(
|
||||
( resolve, reject ) => {
|
||||
getCurrentPosition( resolve, reject, options );
|
||||
}
|
||||
);
|
||||
|
||||
export const checkLocationPermissions = async ( ) => {
|
||||
const permissionResult = permissionResultFromMultiple(
|
||||
await checkMultiple( LOCATION_PERMISSIONS )
|
||||
);
|
||||
|
||||
// TODO: handle case where iOS permissions are not granted
|
||||
if ( Platform.OS !== "android" && permissionResult !== RESULTS.GRANTED ) {
|
||||
return null;
|
||||
}
|
||||
return permissionResult;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Geolocation from "@react-native-community/geolocation";
|
||||
import {
|
||||
screen,
|
||||
userEvent,
|
||||
@@ -18,18 +17,11 @@ import { getPredictionsForImage } from "vision-camera-plugin-inatvision";
|
||||
// working normally
|
||||
jest.unmock( "@react-navigation/native" );
|
||||
|
||||
const mockWatchPosition = jest.fn( ( success, _error, _options ) => {
|
||||
setTimeout( ( ) => success( {
|
||||
coords: {
|
||||
latitude: 1,
|
||||
longitude: 1,
|
||||
accuracy: 9
|
||||
},
|
||||
timestamp: Date.now( )
|
||||
} ), 100 );
|
||||
return 0;
|
||||
} );
|
||||
Geolocation.watchPosition.mockImplementation( mockWatchPosition );
|
||||
const mockFetchUserLocation = jest.fn( () => ( { latitude: 1, longitude: 1, accuracy: 9 } ) );
|
||||
jest.mock( "sharedHelpers/fetchAccurateUserLocation", () => ( {
|
||||
__esModule: true,
|
||||
default: () => mockFetchUserLocation()
|
||||
} ) );
|
||||
|
||||
const mockModelResult = {
|
||||
predictions: [factory( "ModelPrediction", {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Geolocation from "@react-native-community/geolocation";
|
||||
import {
|
||||
useNetInfo
|
||||
} from "@react-native-community/netinfo";
|
||||
@@ -68,20 +67,12 @@ jest.mock( "react-native/Libraries/Utilities/Platform", ( ) => ( {
|
||||
Version: 11
|
||||
} ) );
|
||||
|
||||
const watchPositionSuccess = jest.fn( success => success( {
|
||||
coords: {
|
||||
latitude: 56,
|
||||
longitude: 9,
|
||||
accuracy: 8
|
||||
}
|
||||
const mockFetchUserLocation = jest.fn( () => ( { latitude: 56, longitude: 9, accuracy: 8 } ) );
|
||||
jest.mock( "sharedHelpers/fetchAccurateUserLocation", () => ( {
|
||||
__esModule: true,
|
||||
default: () => mockFetchUserLocation()
|
||||
} ) );
|
||||
|
||||
const mockWatchPosition = jest.fn( ( success, _error, _options ) => {
|
||||
setTimeout( () => watchPositionSuccess( success ), 100 );
|
||||
return 0;
|
||||
} );
|
||||
Geolocation.watchPosition.mockImplementation( mockWatchPosition );
|
||||
|
||||
// We're explicitly testing navigation here so we want react-navigation
|
||||
// working normally
|
||||
jest.unmock( "@react-navigation/native" );
|
||||
@@ -178,21 +169,13 @@ const navigateToSuggestionsForObservationViaObsEdit = async observation => {
|
||||
await actor.press( addIdButton );
|
||||
};
|
||||
|
||||
const navigateToSuggestionsViaAICamera = async ( options = {} ) => {
|
||||
const navigateToSuggestionsViaAICamera = async ( ) => {
|
||||
const tabBar = await screen.findByTestId( "CustomTabBar" );
|
||||
const addObsButton = await within( tabBar ).findByLabelText( "Add observations" );
|
||||
await actor.press( addObsButton );
|
||||
const cameraButton = await screen.findByLabelText( /AI Camera/ );
|
||||
await actor.press( cameraButton );
|
||||
|
||||
if ( options.waitForLocation ) {
|
||||
await waitFor( ( ) => {
|
||||
expect( Geolocation.watchPosition ).toHaveReturnedWith( 0 );
|
||||
} );
|
||||
await waitFor( ( ) => {
|
||||
expect( watchPositionSuccess ).toHaveReturned( );
|
||||
}, 100 );
|
||||
}
|
||||
const takePhotoButton = await screen.findByLabelText( /Take photo/ );
|
||||
await actor.press( takePhotoButton );
|
||||
const addIDButton = await screen.findByText( /ADD AN ID/ );
|
||||
@@ -345,21 +328,19 @@ describe( "from AICamera directly", ( ) => {
|
||||
describe( "suggestions with location", ( ) => {
|
||||
it( "should call score_image with location parameters on first render", async ( ) => {
|
||||
await setupAppWithSignedInUser( );
|
||||
await navigateToSuggestionsViaAICamera( { waitForLocation: true } );
|
||||
// const ignoreLocationButton = await screen.findByText( /IGNORE LOCATION/ );
|
||||
// expect( ignoreLocationButton ).toBeVisible( );
|
||||
// await waitFor( ( ) => {
|
||||
// expect( inatjs.computervision.score_image ).toHaveBeenCalledWith(
|
||||
// expect.objectContaining( {
|
||||
// // Don't care about fields here
|
||||
// fields: expect.any( Object ),
|
||||
// image: expect.any( Object ),
|
||||
// lat: 56,
|
||||
// lng: 9
|
||||
// } ),
|
||||
// expect.anything( )
|
||||
// );
|
||||
// } );
|
||||
await navigateToSuggestionsViaAICamera( );
|
||||
await waitFor( ( ) => {
|
||||
expect( inatjs.computervision.score_image ).toHaveBeenCalledWith(
|
||||
expect.objectContaining( {
|
||||
// Don't care about fields here
|
||||
fields: expect.any( Object ),
|
||||
image: expect.any( Object ),
|
||||
lat: 56,
|
||||
lng: 9
|
||||
} ),
|
||||
expect.anything( )
|
||||
);
|
||||
} );
|
||||
} );
|
||||
} );
|
||||
|
||||
@@ -370,6 +351,7 @@ describe( "from AICamera directly", ( ) => {
|
||||
hasPermissions: false,
|
||||
renderPermissionsGate: jest.fn( )
|
||||
} ) );
|
||||
mockFetchUserLocation.mockReturnValue( null );
|
||||
await setupAppWithSignedInUser( );
|
||||
await navigateToSuggestionsViaAICamera( );
|
||||
await waitFor( ( ) => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Geolocation from "@react-native-community/geolocation";
|
||||
import {
|
||||
screen,
|
||||
userEvent,
|
||||
@@ -97,6 +96,12 @@ afterEach( ( ) => {
|
||||
signOut( { realm: global.mockRealms[__filename] } );
|
||||
} );
|
||||
|
||||
const mockFetchUserLocation = jest.fn( () => ( { latitude: 56, longitude: 9, accuracy: 8 } ) );
|
||||
jest.mock( "sharedHelpers/fetchAccurateUserLocation", () => ( {
|
||||
__esModule: true,
|
||||
default: () => mockFetchUserLocation()
|
||||
} ) );
|
||||
|
||||
const actor = userEvent.setup( );
|
||||
|
||||
const navToAICamera = async ( ) => {
|
||||
@@ -139,14 +144,6 @@ describe( "AICamera navigation with advanced user layout", ( ) => {
|
||||
|
||||
describe( "to Suggestions", ( ) => {
|
||||
beforeEach( ( ) => {
|
||||
const mockWatchPosition = jest.fn( ( success, _error, _options ) => success( {
|
||||
coords: {
|
||||
latitude: 56,
|
||||
longitude: 9,
|
||||
accuracy: 8
|
||||
}
|
||||
} ) );
|
||||
Geolocation.watchPosition.mockImplementation( mockWatchPosition );
|
||||
jest.spyOn( usePredictions, "default" ).mockImplementation( () => ( {
|
||||
handleTaxaDetected: jest.fn( ),
|
||||
modelLoaded: true,
|
||||
|
||||
@@ -41,7 +41,7 @@ const mockObservations = [
|
||||
];
|
||||
|
||||
const mockFetchUserLocation = jest.fn( () => ( { latitude: 37, longitude: 34 } ) );
|
||||
jest.mock( "sharedHelpers/fetchUserLocation", () => ( {
|
||||
jest.mock( "sharedHelpers/fetchCoarseUserLocation", () => ( {
|
||||
__esModule: true,
|
||||
default: () => mockFetchUserLocation()
|
||||
} ) );
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import Geolocation from "@react-native-community/geolocation";
|
||||
import {
|
||||
screen,
|
||||
userEvent,
|
||||
@@ -49,6 +48,12 @@ beforeAll( async () => {
|
||||
|
||||
const actor = userEvent.setup( );
|
||||
|
||||
const mockFetchUserLocation = jest.fn( () => ( { latitude: 56, longitude: 9, accuracy: 8 } ) );
|
||||
jest.mock( "sharedHelpers/fetchAccurateUserLocation", () => ( {
|
||||
__esModule: true,
|
||||
default: () => mockFetchUserLocation()
|
||||
} ) );
|
||||
|
||||
const navigateToCamera = async ( ) => {
|
||||
await waitFor( ( ) => {
|
||||
global.timeTravel( );
|
||||
@@ -88,14 +93,6 @@ describe( "StandardCamera navigation with advanced user layout", ( ) => {
|
||||
} );
|
||||
|
||||
it( "should advance to ObsEdit when photo taken and checkmark tapped", async () => {
|
||||
const mockWatchPosition = jest.fn( ( success, _error, _options ) => success( {
|
||||
coords: {
|
||||
latitude: 56,
|
||||
longitude: 9,
|
||||
accuracy: 8
|
||||
}
|
||||
} ) );
|
||||
Geolocation.watchPosition.mockImplementation( mockWatchPosition );
|
||||
renderApp( );
|
||||
await navigateToCamera( );
|
||||
const takePhotoButton = await screen.findByLabelText( /Take photo/ );
|
||||
@@ -120,14 +117,6 @@ describe( "StandardCamera navigation with advanced user layout", ( ) => {
|
||||
} );
|
||||
|
||||
it( "should advance to Suggestions when photo taken and checkmark tapped", async ( ) => {
|
||||
const mockWatchPosition = jest.fn( ( success, _error, _options ) => success( {
|
||||
coords: {
|
||||
latitude: 56,
|
||||
longitude: 9,
|
||||
accuracy: 8
|
||||
}
|
||||
} ) );
|
||||
Geolocation.watchPosition.mockImplementation( mockWatchPosition );
|
||||
renderApp( );
|
||||
await navigateToCamera( );
|
||||
const takePhotoButton = await screen.findByLabelText( /Take photo/ );
|
||||
|
||||
@@ -23,6 +23,12 @@ jest.mock( "react-native/Libraries/Utilities/Platform", ( ) => ( {
|
||||
Version: 11
|
||||
} ) );
|
||||
|
||||
const mockFetchUserLocation = jest.fn( () => ( { latitude: 56, longitude: 9, accuracy: 8 } ) );
|
||||
jest.mock( "sharedHelpers/fetchAccurateUserLocation", () => ( {
|
||||
__esModule: true,
|
||||
default: () => mockFetchUserLocation()
|
||||
} ) );
|
||||
|
||||
// We're explicitly testing navigation here so we want react-navigation
|
||||
// working normally
|
||||
jest.unmock( "@react-navigation/native" );
|
||||
|
||||
Reference in New Issue
Block a user