diff --git a/__mocks__/react-native-geolocation-service.js b/__mocks__/react-native-geolocation-service.js new file mode 100644 index 000000000000..f4631519a163 --- /dev/null +++ b/__mocks__/react-native-geolocation-service.js @@ -0,0 +1,9 @@ +export default { + addListener: jest.fn(), + getCurrentPosition: jest.fn(), + removeListeners: jest.fn(), + requestAuthorization: jest.fn(), + setConfiguration: jest.fn(), + startObserving: jest.fn(), + stopObserving: jest.fn(), +}; diff --git a/__mocks__/react-native-permissions.js b/__mocks__/react-native-permissions.js new file mode 100644 index 000000000000..e0355503f1e0 --- /dev/null +++ b/__mocks__/react-native-permissions.js @@ -0,0 +1,65 @@ +const {PERMISSIONS} = require('react-native-permissions/dist/commonjs/permissions'); +const {RESULTS} = require('react-native-permissions/dist/commonjs/results'); + +export {PERMISSIONS, RESULTS}; + +export const openLimitedPhotoLibraryPicker = jest.fn((() => {})); +export const openSettings = jest.fn(() => {}); +export const check = jest.fn(() => RESULTS.GRANTED); +export const request = jest.fn(() => RESULTS.GRANTED); +export const checkLocationAccuracy = jest.fn(() => 'full'); +export const requestLocationAccuracy = jest.fn(() => 'full'); + +const notificationOptions = ['alert', 'badge', 'sound', 'carPlay', 'criticalAlert', 'provisional']; + +const notificationSettings = { + alert: true, + badge: true, + sound: true, + carPlay: true, + criticalAlert: true, + provisional: true, + lockScreen: true, + notificationCenter: true, +}; + +export const checkNotifications = jest.fn(() => ({ + status: RESULTS.GRANTED, + settings: notificationSettings, +})); + +export const requestNotifications = jest.fn(options => ({ + status: RESULTS.GRANTED, + settings: options + .filter(option => notificationOptions.includes(option)) + .reduce((acc, option) => ({...acc, [option]: true}), { + lockScreen: true, + notificationCenter: true, + }), +})); + +export const checkMultiple = jest.fn(permissions => permissions.reduce((acc, permission) => ({ + ...acc, + [permission]: RESULTS.GRANTED, +}))); + +export const requestMultiple = jest.fn(permissions => permissions.reduce((acc, permission) => ({ + ...acc, + [permission]: RESULTS.GRANTED, +}))); + +export default { + PERMISSIONS, + RESULTS, + + check, + checkLocationAccuracy, + checkMultiple, + checkNotifications, + openLimitedPhotoLibraryPicker, + openSettings, + request, + requestLocationAccuracy, + requestMultiple, + requestNotifications, +}; diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 01f2ba8ded56..8b7e639b1993 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 97ED3CCB9FD5A5A2AB20CC03 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ExpensifyCash-ExpensifyCashTests/Pods-ExpensifyCash-ExpensifyCashTests-frameworks.sh", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL/OpenSSL.framework/OpenSSL", - "${PODS_XCFRAMEWORKS_BUILD_DIR}/LinkKit/LinkKit.framework/LinkKit", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/LinkKit.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ExpensifyCash-ExpensifyCashTests/Pods-ExpensifyCash-ExpensifyCashTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - AEFD4743761AD0E2373D0494 /* [CP] Check Pods Manifest.lock */ = { + 39ECFD0897ADA7A0E34E0C36 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -405,7 +366,7 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - C9071365B664DE0D85DC5195 /* [CP] Copy Pods Resources */ = { + 7466D6D5FFFCE8DCCD1EB333 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -533,15 +494,35 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ExpensifyCash/Pods-ExpensifyCash-resources.sh\"\n"; showEnvVarsInLog = 0; }; - CF6259710B5A341870372EA2 /* [CP-User] [RNFB] Core Configuration */ = { + 97ED3CCB9FD5A5A2AB20CC03 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); - name = "[CP-User] [RNFB] Core Configuration"; + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ExpensifyCash-ExpensifyCashTests/Pods-ExpensifyCash-ExpensifyCashTests-frameworks.sh", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/OpenSSL/OpenSSL.framework/OpenSSL", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/LinkKit/LinkKit.framework/LinkKit", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OpenSSL.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/LinkKit.framework", + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"info: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"info: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"info: -> RNFB build script started\"\necho \"info: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"info: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"info: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n _RN_ROOT_EXISTS=$(ruby -e \"require 'rubygems';require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\" || echo '')\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n _JSON_OUTPUT_BASE64=$(python -c 'import json,sys,base64;print(base64.b64encode(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"').read())['${_JSON_ROOT}'])))' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\n\n # config.admob_delay_app_measurement_init\n _ADMOB_DELAY_APP_MEASUREMENT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"admob_delay_app_measurement_init\")\n if [[ $_ADMOB_DELAY_APP_MEASUREMENT == \"true\" ]]; then\n _PLIST_ENTRY_KEYS+=(\"GADDelayAppMeasurementInit\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"YES\")\n fi\n\n # config.admob_ios_app_id\n _ADMOB_IOS_APP_ID=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"admob_ios_app_id\")\n if [[ $_ADMOB_IOS_APP_ID ]]; then\n _PLIST_ENTRY_KEYS+=(\"GADApplicationIdentifier\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_ADMOB_IOS_APP_ID\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"info: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"info: <- RNFB build script finished\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ExpensifyCash-ExpensifyCashTests/Pods-ExpensifyCash-ExpensifyCashTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + DA4F8CF5B777BDB8DD1EB45D /* [CP-User] [RNFB] Crashlytics Configuration */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + name = "[CP-User] [RNFB] Crashlytics Configuration"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\n\nif [[ ${PODS_ROOT} ]]; then\n echo \"info: Exec FirebaseCrashlytics Run from Pods\"\n \"${PODS_ROOT}/FirebaseCrashlytics/run\"\nelse\n echo \"info: Exec FirebaseCrashlytics Run from framework\"\n \"${PROJECT_DIR}/FirebaseCrashlytics.framework/run\"\nfi\n"; }; DBBB399CCB345652E314C5BC /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; @@ -563,17 +544,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ExpensifyCash/Pods-ExpensifyCash-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - ED5B8E90A3384FC6A128FB95 /* [CP-User] [RNFB] Crashlytics Configuration */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - name = "[CP-User] [RNFB] Crashlytics Configuration"; - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\n\nif [[ ${PODS_ROOT} ]]; then\n echo \"info: Exec FirebaseCrashlytics Run from Pods\"\n \"${PODS_ROOT}/FirebaseCrashlytics/run\"\nelse\n echo \"info: Exec FirebaseCrashlytics Run from framework\"\n \"${PROJECT_DIR}/FirebaseCrashlytics.framework/run\"\nfi\n"; - }; - F2B8013B9E9ECAB2B509E702 /* [CP] Copy Pods Resources */ = { + E3E26870421B0A221D65F7A4 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -701,6 +672,38 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ExpensifyCash-ExpensifyCashTests/Pods-ExpensifyCash-ExpensifyCashTests-resources.sh\"\n"; showEnvVarsInLog = 0; }; + ED5B8E90A3384FC6A128FB95 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ExpensifyCash-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F928D8F3D996EB54A782A79C /* [CP-User] [RNFB] Core Configuration */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + name = "[CP-User] [RNFB] Core Configuration"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"info: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"info: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"info: -> RNFB build script started\"\necho \"info: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"info: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"info: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n _RN_ROOT_EXISTS=$(ruby -e \"require 'rubygems';require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\" || echo '')\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n _JSON_OUTPUT_BASE64=$(python -c 'import json,sys,base64;print(base64.b64encode(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"').read())['${_JSON_ROOT}'])))' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\n\n # config.admob_delay_app_measurement_init\n _ADMOB_DELAY_APP_MEASUREMENT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"admob_delay_app_measurement_init\")\n if [[ $_ADMOB_DELAY_APP_MEASUREMENT == \"true\" ]]; then\n _PLIST_ENTRY_KEYS+=(\"GADDelayAppMeasurementInit\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"YES\")\n fi\n\n # config.admob_ios_app_id\n _ADMOB_IOS_APP_ID=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"admob_ios_app_id\")\n if [[ $_ADMOB_IOS_APP_ID ]]; then\n _PLIST_ENTRY_KEYS+=(\"GADApplicationIdentifier\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_ADMOB_IOS_APP_ID\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"info: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"info: <- RNFB build script finished\"\n"; + }; FD10A7F022414F080027D42C /* Start Packager */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -736,6 +739,7 @@ buildActionMask = 2147483647; files = ( 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, + 18D050E0262400AF000D658B /* BridgingFile.swift in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -753,8 +757,9 @@ /* Begin XCBuildConfiguration section */ 00E356F61AD99517003FC87E /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = AF728B3FCBC8C2731C4DA7B4 /* Pods-ExpensifyCash-ExpensifyCashTests.debug.xcconfig */; + baseConfigurationReference = CF8C66EE73F9B503DE3E0D58 /* Pods-ExpensifyCash-ExpensifyCashTests.debug.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -777,8 +782,9 @@ }; 00E356F71AD99517003FC87E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E7967C67752432EA2031954F /* Pods-ExpensifyCash-ExpensifyCashTests.release.xcconfig */; + baseConfigurationReference = 1C3425E20B41643777DE7722 /* Pods-ExpensifyCash-ExpensifyCashTests.release.xcconfig */; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; @@ -799,7 +805,7 @@ }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F4BC0E78FF1E9BD8B2D38C66 /* Pods-ExpensifyCash.debug.xcconfig */; + baseConfigurationReference = DDF4204498D793D7FB483440 /* Pods-ExpensifyCash.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -822,6 +828,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.chat.expensify.chat; PRODUCT_NAME = Expensify.cash; PROVISIONING_PROFILE_SPECIFIER = chat_expensify_appstore; + SWIFT_OBJC_BRIDGING_HEADER = "ExpensifyCash-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -831,7 +838,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = A13EE2CFAF952F935D201D2F /* Pods-ExpensifyCash.release.xcconfig */; + baseConfigurationReference = 46BF67CDAA47420D541264C2 /* Pods-ExpensifyCash.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -853,6 +860,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.chat.expensify.chat; PRODUCT_NAME = Expensify.cash; PROVISIONING_PROFILE_SPECIFIER = chat_expensify_appstore; + SWIFT_OBJC_BRIDGING_HEADER = "ExpensifyCash-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; diff --git a/ios/ExpensifyCash/Info.plist b/ios/ExpensifyCash/Info.plist index 95020e9820e9..fe2ae3e2243e 100644 --- a/ios/ExpensifyCash/Info.plist +++ b/ios/ExpensifyCash/Info.plist @@ -88,6 +88,7 @@ UIBackgroundModes + location remote-notification UIFileSharingEnabled diff --git a/ios/Podfile b/ios/Podfile index 8e6678d56384..91bc944897d2 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -4,9 +4,14 @@ require_relative '../node_modules/@react-native-community/cli-platform-ios/nativ platform :ios, '11.0' target 'ExpensifyCash' do + permissions_path = '../node_modules/react-native-permissions/ios' + pod 'Plaid', '~> 2.1.2' - config = use_native_modules! + pod 'Permission-LocationAccuracy', :path => "#{permissions_path}/LocationAccuracy" + pod 'Permission-LocationAlways', :path => "#{permissions_path}/LocationAlways" + pod 'Permission-LocationWhenInUse', :path => "#{permissions_path}/LocationWhenInUse" + config = use_native_modules! use_react_native!(:path => config["reactNativePath"]) target 'ExpensifyCashTests' do diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6b4061284d47..7e562ae6947b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -151,6 +151,12 @@ PODS: - nanopb/decode (1.30906.0) - nanopb/encode (1.30906.0) - OpenSSL-Universal (1.1.180) + - Permission-LocationAccuracy (3.0.1): + - RNPermissions + - Permission-LocationAlways (3.0.1): + - RNPermissions + - Permission-LocationWhenInUse (3.0.1): + - RNPermissions - Plaid (2.1.2) - PromisesObjC (1.2.12) - RCTRequired (0.63.3) @@ -325,6 +331,8 @@ PODS: - React-Core - react-native-document-picker (4.0.0): - React-Core + - react-native-geolocation-service (5.2.0): + - React - react-native-image-picker (2.3.4): - React-Core - react-native-netinfo (5.9.10): @@ -425,6 +433,8 @@ PODS: - RNFBApp - RNGestureHandler (1.9.0): - React-Core + - RNPermissions (3.0.1): + - React-Core - RNReanimated (1.13.2): - React-Core - RNScreens (2.17.1): @@ -463,6 +473,9 @@ DEPENDENCIES: - FlipperKit/SKIOSNetworkPlugin (= 0.75.1) - Folly (from `../node_modules/react-native/third-party-podspecs/Folly.podspec`) - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) + - Permission-LocationAccuracy (from `../node_modules/react-native-permissions/ios/LocationAccuracy`) + - Permission-LocationAlways (from `../node_modules/react-native-permissions/ios/LocationAlways`) + - Permission-LocationWhenInUse (from `../node_modules/react-native-permissions/ios/LocationWhenInUse`) - Plaid (~> 2.1.2) - RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`) - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) @@ -478,6 +491,7 @@ DEPENDENCIES: - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - react-native-config (from `../node_modules/react-native-config`) - react-native-document-picker (from `../node_modules/react-native-document-picker`) + - react-native-geolocation-service (from `../node_modules/react-native-geolocation-service`) - react-native-image-picker (from `../node_modules/react-native-image-picker`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-pdf (from `../node_modules/react-native-pdf`) @@ -505,6 +519,7 @@ DEPENDENCIES: - "RNFBApp (from `../node_modules/@react-native-firebase/app`)" - "RNFBCrashlytics (from `../node_modules/@react-native-firebase/crashlytics`)" - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) + - RNPermissions (from `../node_modules/react-native-permissions`) - RNReanimated (from `../node_modules/react-native-reanimated`) - RNScreens (from `../node_modules/react-native-screens`) - RNSVG (from `../node_modules/react-native-svg`) @@ -550,6 +565,12 @@ EXTERNAL SOURCES: :podspec: "../node_modules/react-native/third-party-podspecs/Folly.podspec" glog: :podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec" + Permission-LocationAccuracy: + :path: "../node_modules/react-native-permissions/ios/LocationAccuracy" + Permission-LocationAlways: + :path: "../node_modules/react-native-permissions/ios/LocationAlways" + Permission-LocationWhenInUse: + :path: "../node_modules/react-native-permissions/ios/LocationWhenInUse" RCTRequired: :path: "../node_modules/react-native/Libraries/RCTRequired" RCTTypeSafety: @@ -574,6 +595,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-config" react-native-document-picker: :path: "../node_modules/react-native-document-picker" + react-native-geolocation-service: + :path: "../node_modules/react-native-geolocation-service" react-native-image-picker: :path: "../node_modules/react-native-image-picker" react-native-netinfo: @@ -628,6 +651,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-firebase/crashlytics" RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" + RNPermissions: + :path: "../node_modules/react-native-permissions" RNReanimated: :path: "../node_modules/react-native-reanimated" RNScreens: @@ -667,6 +692,9 @@ SPEC CHECKSUMS: libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 nanopb: 59317e09cf1f1a0af72f12af412d54edf52603fc OpenSSL-Universal: 1aa4f6a6ee7256b83db99ec1ccdaa80d10f9af9b + Permission-LocationAccuracy: 76669f87b4c276f5ae803cc0ddd1862a4c0e9dd8 + Permission-LocationAlways: a274bc04bb386068782468dbdaca3859f51634ca + Permission-LocationWhenInUse: 3a2b0dbc167d79e8e920a4377ff9520cdc108407 Plaid: c02276ccc630a726a9ed790bf923d29839ff4017 PromisesObjC: 3113f7f76903778cf4a0586bd1ab89329a0b7b97 RCTRequired: 48884c74035a0b5b76dbb7a998bd93bcfc5f2047 @@ -680,14 +708,15 @@ SPEC CHECKSUMS: React-jsiexecutor: b56c03e61c0dd5f5801255f2160a815f4a53d451 React-jsinspector: 8e68ffbfe23880d3ee9bafa8be2777f60b25cbe2 react-native-config: d8b45133fd13d4f23bd2064b72f6e2c08b2763ed - react-native-document-picker: b3e78a8f7fef98b5cb069f20fc35797d55e68e28 - react-native-image-picker: 32d1ad2c0024ca36161ae0d5c2117e2d6c441f11 - react-native-netinfo: 52cf0ee8342548a485e28f4b09e56b477567244d + react-native-document-picker: 0bba80cc56caab1f67dbaa81ff557e3a9b7f2b9f + react-native-geolocation-service: 7c9436da6dfdecd9526c62eac62ea2bc3f0cc8ea + react-native-image-picker: c6d75c4ab2cf46f9289f341242b219cb3c1180d3 + react-native-netinfo: 30fb89fa913c342be82a887b56e96be6d71201dd react-native-pdf: 4b5a9e4465a6a3b399e91dc4838eb44ddf716d1f - react-native-plaid-link-sdk: 1a6593e2d3d790e8113c29178d883eb883f8c032 - react-native-progress-bar-android: ce95a69f11ac580799021633071368d08aaf9ad8 - react-native-progress-view: 5816e8a6be812c2b122c6225a2a3db82d9008640 - react-native-safe-area-context: 01158a92c300895d79dee447e980672dc3fb85a6 + react-native-plaid-link-sdk: 59b7376efca9f00e9693321c5cf7c6ab2c567635 + react-native-progress-bar-android: be43138ab7da30d51fc038bafa98e9ed594d0c40 + react-native-progress-view: 21b1e29e70c7559c16c9e0a04c4adc19fce6ede2 + react-native-safe-area-context: 79fea126c6830c85f65947c223a5e3058a666937 React-RCTActionSheet: 53ea72699698b0b47a6421cb1c8b4ab215a774aa React-RCTAnimation: 1befece0b5183c22ae01b966f5583f42e69a83c2 React-RCTBlob: 0b284339cbe4b15705a05e2313a51c6d8b51fa40 @@ -699,22 +728,23 @@ SPEC CHECKSUMS: React-RCTVibration: 8e9fb25724a0805107fc1acc9075e26f814df454 ReactCommon: 4167844018c9ed375cc01a843e9ee564399e53c3 rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba - RNBootSplash: 3123ba68fe44d8be09a014e89cc8f0f55b68a521 - RNCAsyncStorage: cb9a623793918c6699586281f0b51cbc38f046f9 - RNCClipboard: 5e299c6df8e0c98f3d7416b86ae563d3a9f768a3 - RNCMaskedView: f5c7d14d6847b7b44853f7acb6284c1da30a3459 + RNBootSplash: 24175aa28fe203b10c48dc34e78d946fd33c77af + RNCAsyncStorage: b03032fdbdb725bea0bd9e5ec5a7272865ae7398 + RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495 + RNCMaskedView: 5a8ec07677aa885546a0d98da336457e2bea557f RNCPicker: 6780c753e9e674065db90d9c965920516402579d RNFBAnalytics: 2dc4dd9e2445faffca041b10447a23a71dcdabf8 RNFBApp: 7eacc7da7ab19f96c05e434017d44a9f09410da8 RNFBCrashlytics: 4870c14cf8833053b6b5648911abefe1923854d2 RNGestureHandler: 9b7e605a741412e20e13c512738a31bd1611759b + RNPermissions: 4c8a37b4dde50f1f152bf8cd08c4a43d2355829e RNReanimated: e03f7425cb7a38dcf1b644d680d1bfc91c3337ad RNScreens: b6c9607e6fe47c1b6e2f1910d2acd46dd7ecea3a RNSVG: ce9d996113475209013317e48b05c21ee988d42e - urbanairship-react-native: dfb6dc22b2f41ccaadd636b73d51b448cd1b2bbc + urbanairship-react-native: afab7684561909be2a6a2a52baa3435776d050de Yoga: 7d13633d129fd179e01b8953d38d47be90db185a YogaKit: f782866e155069a2cca2517aafea43200b01fd5a -PODFILE CHECKSUM: 6eb5d43e785faa9b1fe52688d5bf7a328bd1b1cf +PODFILE CHECKSUM: e2cbcef0a80ad10b622900511a519e73949d415d COCOAPODS: 1.10.1 diff --git a/package-lock.json b/package-lock.json index 9b23fbef5be2..42cb4a1e52f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32673,6 +32673,11 @@ "resolved": "https://registry.npmjs.org/react-native-document-picker/-/react-native-document-picker-4.0.0.tgz", "integrity": "sha512-tjIOBBcyjv4j5E1MDL2OvEKNpXxQybLNkjjfpTyDUzek7grZ5eOvSlp6i/Y3EfuIGLByeaw++9R1SZtOij6R7w==" }, + "react-native-geolocation-service": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-native-geolocation-service/-/react-native-geolocation-service-5.2.0.tgz", + "integrity": "sha512-ai7xd6QbLl6WMyEbPfXSaXyYQ/L6CDcPjOZAJYboqwNPclAqxGkzJHJQyvBNy9J410EIrDJg0p9KyaciXmxyCw==" + }, "react-native-gesture-handler": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-1.9.0.tgz", @@ -32760,6 +32765,11 @@ "prop-types": "^15.7.2" } }, + "react-native-permissions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/react-native-permissions/-/react-native-permissions-3.0.1.tgz", + "integrity": "sha512-loCoNEeBeLsrvITZmrkuCUEMFiuwhKuFvkWbC5Imco4bj2hUW7BVI29i8QyxkC53ydRfSR9OtbR3KQIyghWiNQ==" + }, "react-native-picker-select": { "version": "8.0.4", "resolved": "https://registry.npmjs.org/react-native-picker-select/-/react-native-picker-select-8.0.4.tgz", diff --git a/package.json b/package.json index ae815de8d14d..6a7925143ade 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,7 @@ "react-native-bootsplash": "^3.2.0", "react-native-config": "^1.4.0", "react-native-document-picker": "^4.0.0", + "react-native-geolocation-service": "^5.2.0", "react-native-gesture-handler": "1.9.0", "react-native-image-pan-zoom": "^2.1.12", "react-native-image-picker": "^2.3.3", @@ -81,6 +82,7 @@ "react-native-modal": "^11.5.6", "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#586c76e7b90dbbde051d7ec7adbc4d53b2d51cd1", "react-native-pdf": "^6.2.2", + "react-native-permissions": "^3.0.1", "react-native-picker-select": "8.0.4", "react-native-plaid-link-sdk": "^7.0.5", "react-native-reanimated": "1.13.2", diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index 6ee6ba2a626d..03119bd6a65b 100755 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -42,6 +42,10 @@ export default { // Contains all the personalDetails the user has access to PERSONAL_DETAILS: 'personalDetails', + // Contains a list of all currencies available to the user - user can + // select a currency based on the list + CURRENCY_LIST: 'currencyList', + // Indicates whether an update is available and ready to beinstalled. UPDATE_AVAILABLE: 'updateAvailable', diff --git a/src/ROUTES.js b/src/ROUTES.js index 69223366be97..87dd0cdb4cc4 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -24,9 +24,13 @@ export default { REPORT, REPORT_WITH_ID: 'r/:reportID', getReportRoute: reportID => `r/${reportID}`, + IOU_BILL_CURRENCY: 'iou/split/:reportID/currency', + IOU_REQUEST_CURRENCY: 'iou/request/:reportID/currency', + getIouRequestCurrencyRoute: reportID => `iou/request/${reportID}/currency`, + getIouBillCurrencyRoute: reportID => `iou/split/${reportID}/currency`, IOU_REQUEST: 'iou/request/:reportID', - getIouRequestRoute: reportID => `iou/request/${reportID}`, IOU_BILL: 'iou/split/:reportID', + getIouRequestRoute: reportID => `iou/request/${reportID}`, getIouSplitRoute: reportID => `iou/split/${reportID}`, IOU_DETAILS: 'iou/details', IOU_DETAILS_WITH_IOU_REPORT_ID: 'iou/details/:chatReportID/:iouReportID/', diff --git a/src/components/IOUConfirmationList.js b/src/components/IOUConfirmationList.js index ace33de1ac36..739b866e62c4 100755 --- a/src/components/IOUConfirmationList.js +++ b/src/components/IOUConfirmationList.js @@ -23,7 +23,16 @@ const propTypes = { /** Callback to inform parent modal of success */ onConfirm: PropTypes.func.isRequired, - /** Callback to update comment from IOUModal */ + // User's currency preference + selectedCurrency: PropTypes.shape({ + // Currency code for the selected currency + currencyCode: PropTypes.string, + + // Currency symbol for the selected currency + currencySymbol: PropTypes.string, + }).isRequired, + + // Callback to update comment from IOUModal onUpdateComment: PropTypes.func, /** Comment value from IOUModal */ @@ -38,12 +47,7 @@ const propTypes = { /** IOU amount */ iouAmount: PropTypes.string.isRequired, - /** Selected currency from the user - Remove eslint disable after currency symbol is available */ - // eslint-disable-next-line react/no-unused-prop-types - selectedCurrency: PropTypes.string.isRequired, - - /** Selected participants from IOUMOdal with login */ + // Selected participants from IOUModal with login participants: PropTypes.arrayOf(PropTypes.shape({ login: PropTypes.string.isRequired, alternateText: PropTypes.string, @@ -111,12 +115,12 @@ class IOUConfirmationList extends Component { // Convert from cent to bigger form // USD is temporary and there must be support for other currencies in the future - `$${this.calculateAmount(true) / 100}`, + `${this.props.selectedCurrency.currencySymbol}${this.calculateAmount(true) / 100}`, ); // Cents is temporary and there must be support for other currencies in the future const formattedParticipants = getIOUConfirmationOptionsFromParticipants(this.props.participants, - `$${this.calculateAmount() / 100}`); + `${this.props.selectedCurrency.currencySymbol}${this.calculateAmount() / 100}`); sections.push({ title: this.props.translate('iOUConfirmationList.whoPaid'), @@ -133,7 +137,7 @@ class IOUConfirmationList extends Component { } else { // $ Should be replaced by currency symbol once available const formattedParticipants = getIOUConfirmationOptionsFromParticipants(this.props.participants, - `$${this.props.iouAmount}`); + `${this.props.selectedCurrency.currencySymbol}${this.props.iouAmount}`); sections.push({ title: this.props.translate('common.to').toUpperCase(), @@ -264,8 +268,14 @@ class IOUConfirmationList extends Component { isLoading={this.props.iou.loading} text={this.props.hasMultipleParticipants ? this.props.translate('common.split') - : this.props.translate('iou.request', {amount: this.props.iouAmount})} - onPress={() => this.props.onConfirm(this.getSplits())} + : this.props.translate('iou.request', + { + amount: this.props.numberFormat(this.props.iouAmount, { + style: 'currency', + currency: this.props.selectedCurrency.currencyCode, + }), + })} + onClick={() => this.props.onConfirm(this.getSplits())} /> diff --git a/src/languages/en.js b/src/languages/en.js index ea9b5061e42f..d1895aff0d3b 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -52,6 +52,10 @@ export default { whoWasThere: 'WHO WAS THERE?', whatsItFor: 'WHAT\'S IT FOR?', }, + iOUCurrencySelection: { + selectCurrency: 'Select a Currency', + allCurrencies: 'ALL CURRENCIES', + }, optionsSelector: { nameEmailOrPhoneNumber: 'Name, email, or phone number', }, diff --git a/src/libs/API.js b/src/libs/API.js index a6770801a185..e942b2a3683b 100644 --- a/src/libs/API.js +++ b/src/libs/API.js @@ -783,6 +783,24 @@ function BankAccount_Create(parameters) { return Network.post(commandName, parameters, CONST.NETWORK.METHOD.POST, true); } +/** + * @param {object} parameters + * @param {number} [parameters.latitude] + * @param {number} [parameters.longitude] + * @returns {Promise} + */ +function GetPreferredCurrency(parameters) { + const commandName = 'GetPreferredCurrency'; + return Network.post(commandName, parameters); +} + +/** + * @returns {Promise} + */ +function GetCurrencyList() { + return Mobile_GetConstants({data: ['currencyList']}); +} + export { getAuthToken, Authenticate, @@ -824,4 +842,6 @@ export { CreateIOUSplit, ValidateEmail, Wallet_GetOnfidoSDKToken, + GetPreferredCurrency, + GetCurrencyList, }; diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index f9c8e072fd75..15afffdb1291 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -117,7 +117,8 @@ class AuthScreens extends React.Component { PersonalDetails.fetchPersonalDetails(); User.getUserDetails(); User.getBetas(); - fetchAllReports(true, true); + PersonalDetails.fetchCurrencyPreferences(); + fetchAllReports(true, true, true); fetchCountryCodeByRequestIP(); BankAccounts.fetchBankAccountList(); UnreadIndicatorUpdater.listenForReportChanges(); diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 2fbbca0e6dae..d2af35edba66 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -17,6 +17,7 @@ import SettingsAppDownloadLinks from '../../../pages/settings/AppDownloadLinks'; import SettingsPasswordPage from '../../../pages/settings/PasswordPage'; import SettingsPaymentsPage from '../../../pages/settings/PaymentsPage'; import SettingsAddSecondaryLoginPage from '../../../pages/settings/AddSecondaryLoginPage'; +import IOUCurrencySelection from '../../../pages/iou/IOUCurrencySelection'; import ReportParticipantsPage from '../../../pages/ReportParticipantsPage'; import AddBankAccountPage from '../../../pages/AddBankAccountPage'; @@ -54,11 +55,19 @@ function createModalStackNavigator(screens) { const IOUBillStackNavigator = createModalStackNavigator([{ Component: IOUBillPage, name: 'IOU_Bill_Root', +}, +{ + Component: IOUCurrencySelection, + name: 'IOU_Bill_Currency', }]); const IOURequestModalStackNavigator = createModalStackNavigator([{ Component: IOURequestPage, name: 'IOU_Request_Root', +}, +{ + Component: IOUCurrencySelection, + name: 'IOU_Request_Currency', }]); const IOUDetailsModalStackNavigator = createModalStackNavigator([{ diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index 48039032d008..d2267f5131d5 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -90,11 +90,13 @@ export default { IOU_Request: { screens: { IOU_Request_Root: ROUTES.IOU_REQUEST, + IOU_Request_Currency: ROUTES.IOU_REQUEST_CURRENCY, }, }, IOU_Bill: { screens: { IOU_Bill_Root: ROUTES.IOU_BILL, + IOU_Bill_Currency: ROUTES.IOU_BILL_CURRENCY, }, }, IOU_Details: { diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index f53835f9a116..20171a4c95d1 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -482,6 +482,24 @@ function getHeaderMessage(hasSelectableOptions, hasUserToInvite, searchValue, ma return ''; } +/** + * Returns the currency list for sections display + * + * @param {Object} currencyOptions + * @param {String} searchValue + * @param {Object} selectedCurrency + * @returns {Array} + */ +function getCurrencyListForSections(currencyOptions, searchValue) { + const filteredOptions = currencyOptions.filter(currencyOption => ( + isSearchStringMatch(searchValue, currencyOption.searchText))); + + return { + // returns filtered options i.e. options with string match if search text is entered + currencyOptions: filteredOptions, + }; +} + export { getSearchOptions, getNewChatOptions, @@ -489,6 +507,7 @@ export { getSidebarOptions, getHeaderMessage, getPersonalDetailsForLogins, + getCurrencyListForSections, getIOUConfirmationOptionsFromMyPersonalDetail, getIOUConfirmationOptionsFromParticipants, }; diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index cdd3fe381f7f..4f65c96b2ee7 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -2,6 +2,7 @@ import _ from 'underscore'; import lodashGet from 'lodash/get'; import lodashMerge from 'lodash/merge'; import Onyx from 'react-native-onyx'; +import Geolocation from 'react-native-geolocation-service'; import Str from 'expensify-common/lib/str'; import ONYXKEYS from '../../ONYXKEYS'; import md5 from '../md5'; @@ -236,6 +237,56 @@ function setPersonalDetails(details) { mergeLocalPersonalDetails(details); } +/** + * Sets the onyx with the currency list from the network + * @returns {Object} + */ +function getCurrencyList() { + return API.GetCurrencyList() + .then((data) => { + const currencyListObject = JSON.parse(data.currencyList); + Onyx.merge(ONYXKEYS.CURRENCY_LIST, currencyListObject); + return currencyListObject; + }); +} + +/** + * Fetches the Currency preferences based on location + * @param {bool} withLocation + */ +function fetchCurrencyPreferences(withLocation) { + let coords = {}; + let currency = ''; + + if (withLocation) { + Geolocation.getCurrentPosition((position) => { + coords = { + longitude: position.coords.longitude, + latitude: position.coords.latitude, + }; + }); + Onyx.merge(ONYXKEYS.MY_PERSONAL_DETAILS, + { + isCurrencyPreferencesSaved: true, + }); + } + + API.GetPreferredCurrency({...coords}) + .then((data) => { + currency = data.currency; + }) + .then(API.GetCurrencyList) + .then(getCurrencyList) + .then((currencyList) => { + Onyx.merge(ONYXKEYS.MY_PERSONAL_DETAILS, + { + preferredCurrencyCode: currency, + preferredCurrencySymbol: currencyList[currency].symbol, + }); + }) + .catch(error => console.debug(`Error fetching currency preference: , ${error}`)); +} + /** * Sets the user's avatar image * @@ -273,4 +324,6 @@ export { setPersonalDetails, setAvatar, deleteAvatar, + fetchCurrencyPreferences, + getCurrencyList, }; diff --git a/src/pages/home/sidebar/OptionRow.js b/src/pages/home/sidebar/OptionRow.js index 28532fc42f2d..d6bdb9b34ef2 100644 --- a/src/pages/home/sidebar/OptionRow.js +++ b/src/pages/home/sidebar/OptionRow.js @@ -123,7 +123,8 @@ const OptionRow = ({ {displayName: (isMultipleParticipant ? firstName : displayName) || login, tooltip: login} ), ); - const fullTitle = displayNamesWithTooltips.map(({displayName}) => displayName).join(', '); + const fullTitle = option.text ? option.text + : displayNamesWithTooltips.map(({displayName}) => displayName).join(', '); return ( {hovered => ( diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js new file mode 100644 index 000000000000..c0d3cd0430ff --- /dev/null +++ b/src/pages/iou/IOUCurrencySelection.js @@ -0,0 +1,243 @@ +import React, {Component} from 'react'; +import {Pressable, SectionList, View} from 'react-native'; +import PropTypes from 'prop-types'; +import Onyx, {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; +import styles from '../../styles/styles'; +import {fetchCurrencyPreferences, getCurrencyList} from '../../libs/actions/PersonalDetails'; +import ONYXKEYS from '../../ONYXKEYS'; +import {getCurrencyListForSections} from '../../libs/OptionsListUtils'; +import Text from '../../components/Text'; +import OptionRow from '../home/sidebar/OptionRow'; +import themeColors from '../../styles/themes/default'; +import TextInputWithFocusStyles from '../../components/TextInputWithFocusStyles'; +import Navigation from '../../libs/Navigation/Navigation'; +import ScreenWrapper from '../../components/ScreenWrapper'; +import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; +import compose from '../../libs/compose'; +import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; + +/** + * IOU Currency selection for selecting currency + */ +const propTypes = { + + // The personal details of the person who is logged in + myPersonalDetails: PropTypes.shape({ + + // Preferred Currency Code of the current user + preferredCurrencyCode: PropTypes.string, + + // Currency Symbol of the Preferred Currency + preferredCurrencySymbol: PropTypes.string, + + // Whether preferences for the currency are saved + isCurrencyPreferencesSaved: PropTypes.bool, + }), + + // The currency list constant object from Onyx + currencyList: PropTypes.objectOf(PropTypes.shape({ + // Symbol for the currency + symbol: PropTypes.string, + + // Name of the currency + name: PropTypes.string, + + // ISO4217 Code for the currency + ISO4217: PropTypes.string, + })), + ...withLocalizePropTypes, +}; + +const defaultProps = { + myPersonalDetails: {preferredCurrencyCode: 'USD', preferredCurrencySymbol: '$'}, + currencyList: {}, +}; + +class IOUCurrencySelection extends Component { + constructor(props) { + super(props); + + const {currencyOptions} = getCurrencyListForSections(this.getCurrencyOptions(this.props.currencyList), + ''); + + this.state = { + searchValue: '', + currencyData: currencyOptions, + selectedCurrency: { + currencyCode: this.props.myPersonalDetails.preferredCurrencyCode, + currencySymbol: this.props.myPersonalDetails.preferredCurrencySymbol, + }, + }; + this.getCurrencyOptions = this.getCurrencyOptions.bind(this); + this.toggleOption = this.toggleOption.bind(this); + this.getSections = this.getSections.bind(this); + this.confirmCurrencySelection = this.confirmCurrencySelection.bind(this); + this.changeSearchValue = this.changeSearchValue.bind(this); + } + + componentDidMount() { + getCurrencyList(); + if (!this.props.myPersonalDetails.isCurrencyPreferencesSaved) { + fetchCurrencyPreferences(true); + } + } + + /** + * Returns the sections needed for the OptionsSelector + * + * @param {Boolean} maxParticipantsReached + * @returns {Array} + */ + getSections() { + const sections = []; + + sections.push({ + title: this.props.translate('iOUCurrencySelection.allCurrencies'), + data: this.state.currencyData, + shouldShow: true, + indexOffset: 0, + }); + + return sections; + } + + /** + * + * @returns {Object} + */ + getCurrencyOptions() { + const currencyListKeys = _.keys(this.props.currencyList); + const currencyOptions = _.map(currencyListKeys, currencyCode => ({ + text: `${currencyCode} - ${this.props.currencyList[currencyCode].symbol}`, + searchText: `${currencyCode} ${this.props.currencyList[currencyCode].symbol}`, + currencyCode, + })); + return currencyOptions; + } + + /** + * Function which renders a row in the list + * + * @param {String} currencyCode + * + */ + toggleOption(currencyCode) { + this.setState({ + selectedCurrency: { + currencyCode, + currencySymbol: this.props.currencyList[currencyCode].symbol, + }, + }); + } + + /** + * Sets new search value + * @param {String} searchValue + * @return {void} + */ + changeSearchValue(searchValue) { + const {currencyOptions} = getCurrencyListForSections( + this.getCurrencyOptions(this.props.currencyList), + searchValue, + ); + this.setState({ + searchValue, + currencyData: currencyOptions, + }); + } + + /** + * Confirms the selection of currency and sets values in Onyx + * @return {void} + */ + confirmCurrencySelection() { + Onyx.merge(ONYXKEYS.MY_PERSONAL_DETAILS, { + preferredCurrencyCode: this.state.selectedCurrency.currencyCode, + preferredCurrencySymbol: this.state.selectedCurrency.currencySymbol, + }); + Navigation.goBack(); + } + + render() { + return ( + + Navigation.goBack()} + /> + + + + this.textInput = el} + style={[styles.textInput]} + value={this.state.searchValue} + onChangeText={this.changeSearchValue} + placeholder="Search" + placeholderTextColor={themeColors.placeholderText} + /> + + + option.currencyCode} + stickySectionHeadersEnabled={false} + renderItem={({item, key}) => ( + this.toggleOption(item.currencyCode)} + isSelected={item.currencyCode === this.state.selectedCurrency.currencyCode} + showSelectedState + hideAdditionalOptionStates + /> + )} + renderSectionHeader={({section: {title}}) => ( + + + {title} + + + )} + /> + + + + [ + styles.button, + styles.buttonSuccess, + styles.w100, + hovered && styles.buttonSuccessHovered, + ]} + > + + Confirm + + + + + + ); + } +} + +IOUCurrencySelection.propTypes = propTypes; +IOUCurrencySelection.defaultProps = defaultProps; +IOUCurrencySelection.displayName = 'IOUCurrencySelection'; + +export default compose( + withLocalize, + withOnyx({ + currencyList: {key: ONYXKEYS.CURRENCY_LIST}, + myPersonalDetails: {key: ONYXKEYS.MY_PERSONAL_DETAILS}, + }), +)(IOUCurrencySelection); diff --git a/src/pages/iou/IOUModal.js b/src/pages/iou/IOUModal.js index bca1798c0a99..ebc2ae3fbb9a 100755 --- a/src/pages/iou/IOUModal.js +++ b/src/pages/iou/IOUModal.js @@ -30,7 +30,17 @@ const propTypes = { participants: PropTypes.arrayOf(PropTypes.string), }), - /** Holds data related to IOU view state, rather than the underlying IOU data. */ + // The personal details of the person who is logged in + myPersonalDetails: PropTypes.shape({ + + // Preferred Currency Code of the current user + preferredCurrencyCode: PropTypes.string, + + // Currency Symbol of the Preferred Currency + preferredCurrencySymbol: PropTypes.string, + }), + + // Holds data related to IOU view state, rather than the underlying IOU data. iou: PropTypes.shape({ /** Whether or not transaction creation has started */ creatingIOUTransaction: PropTypes.bool, @@ -59,6 +69,10 @@ const defaultProps = { report: { participants: [], }, + myPersonalDetails: { + preferredCurrencyCode: 'USD', + preferredCurrencySymbol: '$', + }, }; // Determines type of step to display within Modal, value provides the title for that page. @@ -94,7 +108,10 @@ class IOUModal extends Component { // amount is currency in decimal format amount: '', - selectedCurrency: 'USD', + selectedCurrency: { + currencyCode: props.myPersonalDetails.preferredCurrencyCode, + currencySymbol: props.myPersonalDetails.preferredCurrencySymbol, + }, comment: '', }; @@ -116,6 +133,14 @@ class IOUModal extends Component { if (prevProps.iou.creatingIOUTransaction && !this.props.iou.creatingIOUTransaction && !this.props.iou.error) { Navigation.dismissModal(); } + + if (prevProps.myPersonalDetails.preferredCurrencyCode + !== this.props.myPersonalDetails.preferredCurrencyCode) { + this.updateSelectedCurrency({ + currencyCode: this.props.myPersonalDetails.preferredCurrencyCode, + currencySymbol: this.props.myPersonalDetails.preferredCurrencySymbol, + }); + } } /** @@ -128,7 +153,13 @@ class IOUModal extends Component { if (currentStepIndex === 1 || currentStepIndex === 2) { return `${this.props.hasMultipleParticipants ? this.props.translate('common.split') - : this.props.translate('iou.request', {amount: this.state.amount})}`; + : this.props.translate('iou.request', + { + amount: this.props.numberFormat(this.state.amount, { + style: 'currency', + currency: this.state.selectedCurrency.currencyCode, + }), + })}`; } if (currentStepIndex === 0) { return this.props.translate(this.props.hasMultipleParticipants ? 'iou.splitBill' : 'iou.requestMoney'); @@ -143,6 +174,16 @@ class IOUModal extends Component { }); } + /** + * Update the selected currency + * @param {Object} selectedCurrency + */ + updateSelectedCurrency(selectedCurrency) { + this.setState({ + selectedCurrency, + }); + } + /** * Navigate to the next IOU step if possible */ @@ -197,7 +238,7 @@ class IOUModal extends Component { // should send in cents to API amount: this.state.amount * 100, - currency: this.state.selectedCurrency, + currency: this.state.selectedCurrency.currencyCode, splits, }); return; @@ -208,7 +249,7 @@ class IOUModal extends Component { // should send in cents to API amount: this.state.amount * 100, - currency: this.state.selectedCurrency, + currency: this.state.selectedCurrency.currencyCode, debtorEmail: this.state.participants[0].login, }); } @@ -254,7 +295,9 @@ class IOUModal extends Component { this.navigateToNextStep(); }} currencySelected={this.currencySelected} + reportID={this.props.route.params.reportID} selectedCurrency={this.state.selectedCurrency} + hasMultipleParticipants={this.props.hasMultipleParticipants} selectedAmount={this.state.amount} navigation={this.props.navigation} /> @@ -302,5 +345,8 @@ export default compose( personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS, }, + myPersonalDetails: { + key: ONYXKEYS.MY_PERSONAL_DETAILS, + }, }), )(IOUModal); diff --git a/src/pages/iou/steps/IOUAmountPage.js b/src/pages/iou/steps/IOUAmountPage.js index 275f5927e6dc..a5613a8bd8ea 100755 --- a/src/pages/iou/steps/IOUAmountPage.js +++ b/src/pages/iou/steps/IOUAmountPage.js @@ -2,6 +2,7 @@ import React from 'react'; import { View, Text, + TouchableOpacity, } from 'react-native'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; @@ -10,21 +11,35 @@ import styles from '../../../styles/styles'; import BigNumberPad from '../../../components/BigNumberPad'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; import TextInputAutoWidth from '../../../components/TextInputAutoWidth'; +import Navigation from '../../../libs/Navigation/Navigation'; +import ROUTES from '../../../ROUTES'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; import compose from '../../../libs/compose'; import Button from '../../../components/Button'; import KeyboardShortcut from '../../../libs/KeyboardShortcut'; const propTypes = { - /** Callback to inform parent modal of success */ + // Whether or not this IOU has multiple participants + hasMultipleParticipants: PropTypes.bool.isRequired, + + /* The ID of the report this screen should display */ + reportID: PropTypes.string.isRequired, + + // Callback to inform parent modal of success onStepComplete: PropTypes.func.isRequired, /** Currency selection will be implemented later */ // eslint-disable-next-line react/no-unused-prop-types currencySelected: PropTypes.func.isRequired, - /** User's currency preference */ - selectedCurrency: PropTypes.string.isRequired, + // User's currency preference + selectedCurrency: PropTypes.shape({ + // Currency code for the selected currency + currencyCode: PropTypes.string, + + // Currency symbol for the selected currency + currencySymbol: PropTypes.string, + }).isRequired, /** Previously selected amount to show if the user comes back to this screen */ selectedAmount: PropTypes.string.isRequired, @@ -134,9 +149,14 @@ class IOUAmountPage extends React.Component { styles.justifyContentCenter, ]} > - - {this.props.selectedCurrency} - + Navigation.navigate(this.props.hasMultipleParticipants + ? ROUTES.getIouBillCurrencyRoute(this.props.reportID) + : ROUTES.getIouRequestCurrencyRoute(this.props.reportID))} + > + + {this.props.selectedCurrency.currencySymbol} + + {this.props.isSmallScreenWidth ? (