From 5ee4a433dfb33da3ff4ba160d00a37220e789a08 Mon Sep 17 00:00:00 2001 From: Amanda Bullington <35536439+albullington@users.noreply.github.com> Date: Wed, 26 Apr 2023 10:51:05 -0700 Subject: [PATCH] Add location picker to ObsEdit (#593) * Crosshairs, location accuracy warnings, text input for location picker * Add LocationPicker tests * UI improvements and code refactor into smaller components; get GMaps working * Add shadows to icons/text boxes * Use debouncing to avoid map jitter when typing in location * Show place results & let user pick new location from web api * Add tests for location picker with remote results * Add gmaps api key to github actions * Try adding manifest placeholders for env variable to work in github actions * Add key to printf in github actions * Try accessing GMAPS_API_KEY a different way * Update android e2e env file step This updates the "Create .env file" step to use printf to print the Google Maps key into a newly created .env file in the GitHub Action runner. Using the same key as in env.example. * Fix newline --------- Co-authored-by: Johannes Klein --- .github/workflows/e2e_android.yml | 3 +- .gitignore | 2 +- android/app/build.gradle | 1 + android/app/src/main/AndroidManifest.xml | 2 + env.example | 4 +- index.js | 3 + ios/Podfile.lock | 6 +- package-lock.json | 44 +--- package.json | 2 +- .../LocationPicker/CrosshairCircle.js | 57 ++++++ .../LocationPicker/DisplayLatLng.js | 47 +++++ src/components/LocationPicker/Footer.js | 45 +++++ .../LocationPicker/LocationPicker.js | 191 ++++++++++++++++++ .../LocationPicker/LocationSearch.js | 83 ++++++++ src/components/LocationPicker/WarningText.js | 53 +++++ src/components/ObsEdit/EvidenceSection.js | 13 +- src/components/ObsEdit/LocationPicker.js | 80 -------- .../SharedComponents/Buttons/CloseButton.js | 7 +- src/components/SharedComponents/SearchBar.js | 67 ++++++ src/components/SharedComponents/index.js | 2 + src/components/UiLibrary.js | 3 +- src/i18n/l10n/en.ftl | 8 + src/i18n/l10n/en.ftl.json | 4 + src/i18n/strings.ftl | 8 + .../BottomTabNavigator/CustomTabBar.js | 1 + src/navigation/BottomTabNavigator/index.js | 10 + src/sharedHelpers/fetchCoordinates.js | 19 ++ src/sharedHelpers/fetchPlaceName.js | 10 +- src/styles/obsEdit/locationPicker.js | 37 ---- tests/factories/RemotePlace.js | 12 ++ .../LocationPicker/LocationPicker.test.js | 129 ++++++++++++ 31 files changed, 787 insertions(+), 166 deletions(-) create mode 100644 src/components/LocationPicker/CrosshairCircle.js create mode 100644 src/components/LocationPicker/DisplayLatLng.js create mode 100644 src/components/LocationPicker/Footer.js create mode 100644 src/components/LocationPicker/LocationPicker.js create mode 100644 src/components/LocationPicker/LocationSearch.js create mode 100644 src/components/LocationPicker/WarningText.js delete mode 100644 src/components/ObsEdit/LocationPicker.js create mode 100644 src/components/SharedComponents/SearchBar.js create mode 100644 src/sharedHelpers/fetchCoordinates.js delete mode 100644 src/styles/obsEdit/locationPicker.js create mode 100644 tests/factories/RemotePlace.js create mode 100644 tests/unit/components/LocationPicker/LocationPicker.test.js diff --git a/.github/workflows/e2e_android.yml b/.github/workflows/e2e_android.yml index ddfc70cc7..fed851e2b 100644 --- a/.github/workflows/e2e_android.yml +++ b/.github/workflows/e2e_android.yml @@ -83,7 +83,8 @@ jobs: E2E_TEST_USERNAME: ${{ secrets.E2E_TEST_USERNAME }} E2E_TEST_PASSWORD: ${{ secrets.E2E_TEST_PASSWORD }} JWT_ANONYMOUS_API_SECRET: ${{ secrets.JWT_ANONYMOUS_API_SECRET }} - run: printf 'API_URL=https://stagingapi.inaturalist.org/v2\nOAUTH_API_URL=https://staging.inaturalist.org\nJWT_ANONYMOUS_API_SECRET=%s\nOAUTH_CLIENT_ID=%s\nOAUTH_CLIENT_SECRET=%s\nE2E_TEST_USERNAME=%s\nE2E_TEST_PASSWORD=%s\n' "JWT_ANONYMOUS_API_SECRET" "$OAUTH_CLIENT_ID" "$OAUTH_CLIENT_SECRET" "$E2E_TEST_USERNAME" "$E2E_TEST_PASSWORD" > .env + GMAPS_API_KEY: ${{ secrets.GMAPS_API_KEY }} + run: printf 'API_URL=https://stagingapi.inaturalist.org/v2\nOAUTH_API_URL=https://staging.inaturalist.org\nJWT_ANONYMOUS_API_SECRET=%s\nOAUTH_CLIENT_ID=%s\nOAUTH_CLIENT_SECRET=%s\nE2E_TEST_USERNAME=%s\nE2E_TEST_PASSWORD=%s\nGMAPS_API_KEY=%s\n' "JWT_ANONYMOUS_API_SECRET" "$OAUTH_CLIENT_ID" "$OAUTH_CLIENT_SECRET" "$E2E_TEST_USERNAME" "$E2E_TEST_PASSWORD" "$GMAPS_API_KEY" > .env - name: Create keystore.properties file env: ANDROID_KEY_STORE_PASSWORD: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }} diff --git a/.gitignore b/.gitignore index 45c103df5..20596ecb2 100644 --- a/.gitignore +++ b/.gitignore @@ -78,4 +78,4 @@ artifacts/ *.log # VisualStudioCode # -.vscode +.vscode \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 0fcec44ed..a64e9eff8 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -129,6 +129,7 @@ android { // Detox Android setup testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + manifestPlaceholders = [ GMAPS_API_KEY:project.env.get("GMAPS_API_KEY") ] // Detox end } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c9d153cb7..43e85432c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -17,6 +17,8 @@ android:theme="@style/AppTheme" android:requestLegacyExternalStorage="true" android:networkSecurityConfig="@xml/network_security_config"> + + { diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 43b773845..03d6c3a97 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -293,7 +293,7 @@ PODS: - React-Core - react-native-mail (6.1.1): - React-Core - - react-native-maps (0.31.1): + - react-native-maps (1.7.1): - React-Core - react-native-netinfo (9.3.9): - React-Core @@ -723,7 +723,7 @@ SPEC CHECKSUMS: react-native-image-resizer: 00ceb0e05586c7aadf061eea676957a6c2ec60fa react-native-keep-awake: acbee258db16483744910f0da3ace39eb9ab47fd react-native-mail: 8fdcd3aef007c33a6877a18eb4cf7447a1d4ce4a - react-native-maps: 8b8bfada2c86205a7f5a07dd1f92f29b33ea83aa + react-native-maps: 667f9b975549c6fa9b1631bf859440f68ebd3b8f react-native-netinfo: 22c082970cbd99071a4e5aa7a612ac20d66b08f0 react-native-orientation-locker: 851f6510d8046ea2f14aa169b1e01fcd309a94ba react-native-render-html: 984dfe2294163d04bf5fe25d7c9f122e60e05ebe @@ -765,4 +765,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 1e393412e8a65a884dff99189d809b73ea234bcd -COCOAPODS: 1.11.3 +COCOAPODS: 1.12.0 diff --git a/package-lock.json b/package-lock.json index 506c1b012..edd64402a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,7 +71,7 @@ "react-native-localize": "^2.2.6", "react-native-logs": "^5.0.1", "react-native-mail": "github:chirag04/react-native-mail", - "react-native-maps": "^0.31.1", + "react-native-maps": "^1.7.1", "react-native-modal": "^13.0.1", "react-native-modal-datetime-picker": "^15.0.0", "react-native-network-logger": "^1.14.1", @@ -8893,16 +8893,6 @@ "node": ">= 0.8" } }, - "node_modules/deprecated-react-native-prop-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-2.3.0.tgz", - "integrity": "sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA==", - "dependencies": { - "@react-native/normalize-color": "*", - "invariant": "*", - "prop-types": "*" - } - }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -21525,16 +21515,15 @@ "license": "MIT" }, "node_modules/react-native-maps": { - "version": "0.31.1", - "resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-0.31.1.tgz", - "integrity": "sha512-vipeOPykqLRMCLcLUCZEB+cTdNSlq88NLb0jChY4UGTY5fgOS7GYWkfswy6bW1ayTRLxJS3zpMGFDUY59/ZrXA==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.7.1.tgz", + "integrity": "sha512-CHVLzL+Q2jiUGgbt4/vosxVI1SukWyaLGjD62VLgR/wZpnH4Umi9ql1bmKDPWcfj2C1QZwMU0Yc7rXTbvZUIiw==", "dependencies": { - "@types/geojson": "^7946.0.7", - "deprecated-react-native-prop-types": "^2.3.0" + "@types/geojson": "^7946.0.10" }, "peerDependencies": { - "react": ">= 16.0", - "react-native": ">= 0.51", + "react": ">= 17.0.1", + "react-native": ">= 0.64.3", "react-native-web": ">= 0.11" }, "peerDependenciesMeta": { @@ -31556,16 +31545,6 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" }, - "deprecated-react-native-prop-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/deprecated-react-native-prop-types/-/deprecated-react-native-prop-types-2.3.0.tgz", - "integrity": "sha512-pWD0voFtNYxrVqvBMYf5gq3NA2GCpfodS1yNynTPc93AYA/KEMGeWDqqeUB6R2Z9ZofVhks2aeJXiuQqKNpesA==", - "requires": { - "@react-native/normalize-color": "*", - "invariant": "*", - "prop-types": "*" - } - }, "destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -40876,12 +40855,11 @@ "from": "react-native-mail@github:chirag04/react-native-mail" }, "react-native-maps": { - "version": "0.31.1", - "resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-0.31.1.tgz", - "integrity": "sha512-vipeOPykqLRMCLcLUCZEB+cTdNSlq88NLb0jChY4UGTY5fgOS7GYWkfswy6bW1ayTRLxJS3zpMGFDUY59/ZrXA==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.7.1.tgz", + "integrity": "sha512-CHVLzL+Q2jiUGgbt4/vosxVI1SukWyaLGjD62VLgR/wZpnH4Umi9ql1bmKDPWcfj2C1QZwMU0Yc7rXTbvZUIiw==", "requires": { - "@types/geojson": "^7946.0.7", - "deprecated-react-native-prop-types": "^2.3.0" + "@types/geojson": "^7946.0.10" } }, "react-native-modal": { diff --git a/package.json b/package.json index f3bf7ddee..1f449c1ce 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "react-native-localize": "^2.2.6", "react-native-logs": "^5.0.1", "react-native-mail": "github:chirag04/react-native-mail", - "react-native-maps": "^0.31.1", + "react-native-maps": "^1.7.1", "react-native-modal": "^13.0.1", "react-native-modal-datetime-picker": "^15.0.0", "react-native-network-logger": "^1.14.1", diff --git a/src/components/LocationPicker/CrosshairCircle.js b/src/components/LocationPicker/CrosshairCircle.js new file mode 100644 index 000000000..0243bdd9d --- /dev/null +++ b/src/components/LocationPicker/CrosshairCircle.js @@ -0,0 +1,57 @@ +// @flow + +import classnames from "classnames"; +import { INatIcon } from "components/SharedComponents"; +import { View } from "components/styledComponents"; +import type { Node } from "react"; +import React from "react"; +import { useTheme } from "react-native-paper"; + +type Props = { + accuracyTest: string, + containerStyle: Object +}; + +const CrosshairCircle = ( { accuracyTest, containerStyle }: Props ): Node => { + const theme = useTheme( ); + + return ( + + + {/* vertical crosshair */} + + {/* horizontal crosshair */} + + + + {accuracyTest === "pass" && ( + + )} + {accuracyTest === "fail" && ( + + )} + + + ); +}; + +export default CrosshairCircle; diff --git a/src/components/LocationPicker/DisplayLatLng.js b/src/components/LocationPicker/DisplayLatLng.js new file mode 100644 index 000000000..176aedeb8 --- /dev/null +++ b/src/components/LocationPicker/DisplayLatLng.js @@ -0,0 +1,47 @@ +// @flow + +import { + Body4 +} from "components/SharedComponents"; +import { View } from "components/styledComponents"; +import type { Node } from "react"; +import React from "react"; +import { useTheme } from "react-native-paper"; + +type Props = { + region: Object, + accuracy: number, + getShadow: Function +}; + +const DisplayLatLng = ( { region, accuracy, getShadow }: Props ): Node => { + const theme = useTheme( ); + const formatDecimal = coordinate => coordinate && coordinate.toFixed( 6 ); + + const displayLocation = ( ) => { + let location = ""; + if ( region.latitude ) { + location += `Lat: ${formatDecimal( region.latitude )}`; + } + if ( region.longitude ) { + location += `, Lon: ${formatDecimal( region.longitude )}`; + } + if ( accuracy ) { + location += `, Acc: ${accuracy.toFixed( 0 )}`; + } + return location; + }; + + return ( + + + {displayLocation( )} + + + ); +}; + +export default DisplayLatLng; diff --git a/src/components/LocationPicker/Footer.js b/src/components/LocationPicker/Footer.js new file mode 100644 index 000000000..49bfaa23a --- /dev/null +++ b/src/components/LocationPicker/Footer.js @@ -0,0 +1,45 @@ +// @flow + +import { useNavigation } from "@react-navigation/native"; +import { + Button, StickyToolbar +} from "components/SharedComponents"; +import { ObsEditContext } from "providers/contexts"; +import type { Node } from "react"; +import React, { + useContext +} from "react"; +import { Platform } from "react-native"; +import useTranslation from "sharedHooks/useTranslation"; + +type Props = { + keysToUpdate: Object, + goBackOnSave: boolean +}; + +const Footer = ( { keysToUpdate, goBackOnSave }: Props ): Node => { + const navigation = useNavigation( ); + const { t } = useTranslation( ); + const { + updateObservationKeys + } = useContext( ObsEditContext ); + + return ( + +