diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ee0d714124fdd..75c0fb37e23e6 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,9 @@ + + + Your camera is used to create chat attachments. NSLocationWhenInUseUsageDescription + NSLocationAlwaysAndWhenInUseUsageDescription + + NSLocationAlwaysUsageDescription + NSPhotoLibraryAddUsageDescription Your camera roll is used to store chat attachments. NSPhotoLibraryUsageDescription diff --git a/ios/Podfile b/ios/Podfile index b8f5255f0274c..4bb3728efbfa6 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -6,8 +6,14 @@ platform :ios, '11.0' target 'ExpensifyCash' do config = use_native_modules! + permissions_path = '../node_modules/react-native-permissions/ios' + use_react_native!(:path => config["reactNativePath"]) + pod 'Permission-LocationAccuracy', :path => "#{permissions_path}/LocationAccuracy" + pod 'Permission-LocationAlways', :path => "#{permissions_path}/LocationAlways" + pod 'Permission-LocationWhenInUse', :path => "#{permissions_path}/LocationWhenInUse" + target 'ExpensifyCashTests' do inherit! :complete # Pods for testing diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 352903821326f..4e7da22b3219d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -153,6 +153,12 @@ PODS: - OpenSSL-Universal (1.0.2.20): - OpenSSL-Universal/Static (= 1.0.2.20) - OpenSSL-Universal/Static (1.0.2.20) + - Permission-LocationAccuracy (3.0.1): + - RNPermissions + - Permission-LocationAlways (3.0.1): + - RNPermissions + - Permission-LocationWhenInUse (3.0.1): + - RNPermissions - PromisesObjC (1.2.11) - RCTRequired (0.63.3) - RCTTypeSafety (0.63.3): @@ -326,6 +332,8 @@ PODS: - React-Core - react-native-document-picker (4.0.0): - React-Core + - react-native-geolocation (2.0.2): + - React - react-native-image-picker (2.3.4): - React-Core - react-native-netinfo (5.9.10): @@ -419,6 +427,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): @@ -457,6 +467,9 @@ DEPENDENCIES: - FlipperKit/SKIOSNetworkPlugin (~> 0.54.0) - 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`) - RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`) - RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`) - React (from `../node_modules/react-native/`) @@ -471,6 +484,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 (from `../node_modules/@react-native-community/geolocation`)" - 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`) @@ -495,6 +509,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`) @@ -539,6 +554,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: @@ -563,6 +584,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-config" react-native-document-picker: :path: "../node_modules/react-native-document-picker" + react-native-geolocation: + :path: "../node_modules/@react-native-community/geolocation" react-native-image-picker: :path: "../node_modules/react-native-image-picker" react-native-netinfo: @@ -611,6 +634,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: @@ -650,6 +675,9 @@ SPEC CHECKSUMS: GoogleUtilities: 7f2f5a07f888cdb145101d6042bc4422f57e70b3 nanopb: 59317e09cf1f1a0af72f12af412d54edf52603fc OpenSSL-Universal: ff34003318d5e1163e9529b08470708e389ffcdd + Permission-LocationAccuracy: e8adff9ede1b23b43b7054a4500113d515fc87a8 + Permission-LocationAlways: 7f7f373d086af7a81b2f4f20d65d29266ca2043b + Permission-LocationWhenInUse: 3ae82a9feb5da4e94e386dba17c7dd3531af9feb PromisesObjC: 8c196f5a328c2cba3e74624585467a557dcb482f RCTRequired: 48884c74035a0b5b76dbb7a998bd93bcfc5f2047 RCTTypeSafety: edf4b618033c2f1c5b7bc3d90d8e085ed95ba2ab @@ -663,6 +691,7 @@ SPEC CHECKSUMS: React-jsinspector: 8e68ffbfe23880d3ee9bafa8be2777f60b25cbe2 react-native-config: d8b45133fd13d4f23bd2064b72f6e2c08b2763ed react-native-document-picker: b3e78a8f7fef98b5cb069f20fc35797d55e68e28 + react-native-geolocation: cbd9d6bd06bac411eed2671810f454d4908484a8 react-native-image-picker: 32d1ad2c0024ca36161ae0d5c2117e2d6c441f11 react-native-netinfo: 52cf0ee8342548a485e28f4b09e56b477567244d react-native-pdf: 4b5a9e4465a6a3b399e91dc4838eb44ddf716d1f @@ -687,6 +716,7 @@ SPEC CHECKSUMS: RNFBApp: 7eacc7da7ab19f96c05e434017d44a9f09410da8 RNFBCrashlytics: 4870c14cf8833053b6b5648911abefe1923854d2 RNGestureHandler: 9b7e605a741412e20e13c512738a31bd1611759b + RNPermissions: eb94f9fdc0a8ecd02fcce0676d56ffb1395d41e1 RNReanimated: e03f7425cb7a38dcf1b644d680d1bfc91c3337ad RNScreens: b6c9607e6fe47c1b6e2f1910d2acd46dd7ecea3a RNSVG: ce9d996113475209013317e48b05c21ee988d42e @@ -694,6 +724,6 @@ SPEC CHECKSUMS: Yoga: 7d13633d129fd179e01b8953d38d47be90db185a YogaKit: f782866e155069a2cca2517aafea43200b01fd5a -PODFILE CHECKSUM: 41b806c7f131f87b716be1f1f9377532d6c9e43a +PODFILE CHECKSUM: c730d417ba39bd38d4b63c8529779f2eff43bf19 COCOAPODS: 1.10.0 diff --git a/package-lock.json b/package-lock.json index 156e66cf2d45e..ebe0a61fe8921 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2080,11 +2080,6 @@ "picomatch": "^2.0.5" } }, - "mime": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", - "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" - }, "pretty-format": { "version": "26.6.2", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", @@ -3792,6 +3787,11 @@ "integrity": "sha512-W/J0fNYVO01tioHjvYWQ9m6RgndVtbElzYozBq1ZPrHO/iCzlqoySHl4gO/fpCl9QEFjvJfjPgtPMTMlsoq5DQ==", "dev": true }, + "@react-native-community/geolocation": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@react-native-community/geolocation/-/geolocation-2.0.2.tgz", + "integrity": "sha512-tTNXRCgnhJBu79mulQwzabXRpDqfh/uaDqfHVpvF0nX4NTpolpy6mvTRiFg7eWFPGRArsnZz1EYp6rHfJWGgEA==" + }, "@react-native-community/masked-view": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/@react-native-community/masked-view/-/masked-view-0.1.10.tgz", @@ -11080,8 +11080,8 @@ } }, "expensify-common": { - "version": "git+https://github.com/Expensify/expensify-common.git#b89c2c591a2f76ae1a3d8a4a2e2d5e5159d65edc", - "from": "git+https://github.com/Expensify/expensify-common.git#b89c2c591a2f76ae1a3d8a4a2e2d5e5159d65edc", + "version": "git+ssh://git@github.com/Expensify/expensify-common.git#b89c2c591a2f76ae1a3d8a4a2e2d5e5159d65edc", + "from": "expensify-common@git+https://github.com/Expensify/expensify-common.git#b89c2c591a2f76ae1a3d8a4a2e2d5e5159d65edc", "requires": { "classnames": "2.2.5", "clipboard": "2.0.4", @@ -11092,7 +11092,7 @@ "react": "16.12.0", "react-dom": "16.12.0", "semver": "^7.3.4", - "simply-deferred": "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5", + "simply-deferred": "simply-deferred@git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5", "underscore": "1.9.1" }, "dependencies": { @@ -21152,19 +21152,19 @@ } }, "react-native-onyx": { - "version": "git+https://github.com/Expensify/react-native-onyx.git#bd59626781393c93226fb80cb45569408e97b67c", - "from": "git+https://github.com/Expensify/react-native-onyx.git#bd59626781393c93226fb80cb45569408e97b67c", + "version": "git+ssh://git@github.com/Expensify/react-native-onyx.git#bd59626781393c93226fb80cb45569408e97b67c", + "from": "react-native-onyx@git+https://github.com/Expensify/react-native-onyx.git#bd59626781393c93226fb80cb45569408e97b67c", "requires": { "@react-native-community/async-storage": "^1.12.1", - "expensify-common": "git+https://github.com/Expensify/expensify-common.git#c7becaa79e10c10da521261ebb83dd3871847e84", + "expensify-common": "expensify-common@git+https://github.com/Expensify/expensify-common.git#c7becaa79e10c10da521261ebb83dd3871847e84", "lodash": "4.17.21", "react": "^16.13.1", "underscore": "^1.11.0" }, "dependencies": { "expensify-common": { - "version": "git+https://github.com/Expensify/expensify-common.git#c7becaa79e10c10da521261ebb83dd3871847e84", - "from": "git+https://github.com/Expensify/expensify-common.git#c7becaa79e10c10da521261ebb83dd3871847e84", + "version": "git+ssh://git@github.com/Expensify/expensify-common.git#c7becaa79e10c10da521261ebb83dd3871847e84", + "from": "expensify-common@git+https://github.com/Expensify/expensify-common.git#c7becaa79e10c10da521261ebb83dd3871847e84", "requires": { "classnames": "2.2.5", "clipboard": "2.0.4", @@ -21175,7 +21175,7 @@ "react": "16.12.0", "react-dom": "16.12.0", "semver": "^7.3.4", - "simply-deferred": "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5", + "simply-deferred": "simply-deferred@git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5", "underscore": "1.9.1" }, "dependencies": { @@ -21225,6 +21225,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", @@ -22750,8 +22755,8 @@ } }, "simply-deferred": { - "version": "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5", - "from": "git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5" + "version": "git+ssh://git@github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5", + "from": "simply-deferred@git+https://github.com/Expensify/simply-deferred.git#77a08a95754660c7bd6e0b6979fdf84e8e831bf5" }, "sirv": { "version": "1.0.11", diff --git a/package.json b/package.json index aad229cea85b5..91bcc4e0e3cda 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@babel/preset-flow": "^7.12.13", "@react-native-community/async-storage": "^1.11.0", "@react-native-community/cli": "4.13.1", + "@react-native-community/geolocation": "^2.0.2", "@react-native-community/masked-view": "^0.1.10", "@react-native-community/netinfo": "^5.9.10", "@react-native-community/progress-bar-android": "^1.0.4", @@ -70,6 +71,7 @@ "react-native-modal": "^11.5.6", "react-native-onyx": "git+https://github.com/Expensify/react-native-onyx.git#bd59626781393c93226fb80cb45569408e97b67c", "react-native-pdf": "^6.2.2", + "react-native-permissions": "^3.0.1", "react-native-picker-select": "8.0.4", "react-native-reanimated": "1.13.2", "react-native-render-html": "^6.0.0-alpha.10", diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js index 8830470585943..40341c68a2ec2 100644 --- a/src/ONYXKEYS.js +++ b/src/ONYXKEYS.js @@ -42,6 +42,9 @@ export default { // an international code COUNTRY_CODE: 'countryCode', + // Saves the currency list obtained from the network + CURRENCY_LIST: 'currencyList', + // Contains all the users settings for the Settings page and sub pages USER: 'user', diff --git a/src/libs/API.js b/src/libs/API.js index e6156fca7b7dc..edd92c5910b81 100644 --- a/src/libs/API.js +++ b/src/libs/API.js @@ -631,6 +631,24 @@ function GetIOUReport(parameters) { return Network.post(commandName, parameters); } +/** + * @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, @@ -661,4 +679,6 @@ export { User_SecondaryLogin_Send, User_UploadAvatar, reauthenticate, + GetPreferredCurrency, + GetCurrencyList, }; diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index 948bac85a5e2f..94f43f87386fa 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -71,6 +71,7 @@ class AuthScreens extends React.Component { // Fetch some data we need on initialization NameValuePair.get(CONST.NVP.PRIORITY_MODE, ONYXKEYS.NVP_PRIORITY_MODE, 'default'); PersonalDetails.fetch(); + PersonalDetails.setCurrencyPreferences(); User.fetch(); User.getBetas(); fetchAllReports(true, true); diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 58793741fc8e5..60d01a97fadc0 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -143,6 +143,32 @@ function isSearchStringMatch(searchValue, searchText) { }); } +/** + * Returns the personal details for an array of logins + * + * @param {Object} currencyListObject + * @param {String} searchValue + * @param {Object} selectedCurrency + * @returns {Array} + */ +function getCurrencyListForSections(currencyListObject, searchValue) { + const currencyListKeys = _.keys(currencyListObject); + const currencyOptions = _.map(currencyListKeys, currencyCode => ({ + text: `${currencyCode} - ${currencyListObject[currencyCode].symbol}`, + searchText: `${currencyCode} ${currencyListObject[currencyCode].symbol}`, + currencyCode, + })); + + const filteredOptions = currencyOptions.filter( + currencyOption => isSearchStringMatch(searchValue, currencyOption.searchText), + ); + + return { + // currency options holds a section for those currencies which are not selected + currencyOptions: filteredOptions, + }; +} + /** * Build the options * @@ -432,4 +458,5 @@ export { getSidebarOptions, getHeaderMessage, getPersonalDetailsForLogins, + getCurrencyListForSections, }; diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index 7c14f6f3fc985..6d3c8d33c14e9 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 Onyx from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; +import Geolocation from '@react-native-community/geolocation'; import ONYXKEYS from '../../ONYXKEYS'; import md5 from '../md5'; import CONST from '../../CONST'; @@ -213,6 +214,54 @@ function setPersonalDetails(details) { Onyx.merge(ONYXKEYS.PERSONAL_DETAILS, formatPersonalDetails({[currentUserEmail]: details})); } +/** + * Gets the preferred currency for the current user + * + * @param {Object} details + */ +function setCurrencyPreferences() { + let coords = {}; + let currency = ''; + + Geolocation.getCurrentPosition((position) => { + coords = { + longitude: position.coords.longitude, + latitude: position.coords.latitude, + }; + }); + + + API.GetPreferredCurrency({...coords}) + .then((data) => { + currency = data.currency; + }) + .then(() => API.GetCurrencyList()) + .then((data) => { + const currencyList = JSON.parse(data.currencyList); + Onyx.merge(ONYXKEYS.CURRENCY_LIST, currencyList); + return currencyList; + }) + .then((currencyList) => { + Onyx.merge(ONYXKEYS.MY_PERSONAL_DETAILS, + {preferredCurrencyCode: currency, preferredCurrencySymbol: currencyList[currency].symbol}); + }) + .catch(error => console.debug(error)); +} + +/** + * Gets the preferred currency for the current user + * + * @param {Object} details + */ +function getCurrencyList() { + API.GetCurrencyList() + .then((data) => { + const currencyListObject = JSON.parse(data.currencyList); + Onyx.merge(ONYXKEYS.CURRENCY_LIST, currencyListObject); + return currencyListObject; + }); +} + /** * Sets the user's avatar image * @@ -237,4 +286,6 @@ export { getDefaultAvatar, setPersonalDetails, setAvatar, + setCurrencyPreferences, + getCurrencyList, }; diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js new file mode 100644 index 0000000000000..cff1e8f8033ff --- /dev/null +++ b/src/pages/iou/IOUCurrencySelection.js @@ -0,0 +1,219 @@ +import React, {Component} from 'react'; +import {Pressable, SectionList, View} from 'react-native'; +import PropTypes from 'prop-types'; +import {withOnyx} from 'react-native-onyx'; +import styles from '../../styles/styles'; +import {getCurrencyList} from '../../libs/actions/PersonalDetails'; +import ONYXKEYS from '../../ONYXKEYS'; +import ScreenWrapper from '../../components/ScreenWrapper'; +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'; + +/** + * IOU Currency selection for selecting currency + */ +const propTypes = { + + // Callback that sets selectedCurrency in IOUModal + onCurrencySelected: PropTypes.func.isRequired, + + // Callback that sets currency in Onyx and dismisses the currency mode + onCurrencyConfirm: PropTypes.func, + + // User's currency preference + selectedCurrency: PropTypes.string.isRequired, + + // 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, + }), + + // The currency list constant object from Onyx + currencyList: PropTypes.objectOf(PropTypes.shape({ + symbol: PropTypes.string, + name: PropTypes.string, + ISO4217: PropTypes.string, + })), +}; + +const defaultProps = { + myPersonalDetails: {preferredCurrencyCode: 'USD', preferredCurrencySymbol: '$'}, + currencyList: {}, + onCurrencyConfirm: null, +}; + +class IOUCurrencySelection extends Component { + constructor(props) { + super(props); + + const {currencyOptions} = getCurrencyListForSections(this.props.currencyList, + ''); + + this.state = { + searchValue: '', + currencyData: currencyOptions, + }; + this.renderItem = this.renderItem.bind(this); + this.getSections = this.getSections.bind(this); + } + + componentDidMount() { + getCurrencyList(); + } + + /** + * Returns the sections needed for the OptionsSelector + * + * @param {Boolean} maxParticipantsReached + * @returns {Array} + */ + getSections() { + const sections = []; + + sections.push({ + title: 'ALL CURRENCIES', + data: this.state.currencyData, + shouldShow: true, + indexOffset: 0, + }); + + return sections; + } + + /** + * Returns the key used by the list + * @param {Object} option + * @return {String} + */ + extractKey(option) { + return option.currencyCode; + } + + /** + * Function which renders a row in the list + * + * @param {String} currencyCode + * + */ + toggleOption(currencyCode) { + this.props.onCurrencySelected({ + currencyCode, + currencySymbol: this.props.currencyList[currencyCode].symbol, + }); + } + + /** + * Function which renders a row in the list + * + * @param {Object} params + * @param {Object} params.item + * + * @return {Component} + */ + renderItem({item, key}) { + return ( + this.toggleOption(item.currencyCode)} + isSelected={item.currencyCode === this.props.selectedCurrency.currencyCode} + showSelectedState + hideAdditionalOptionStates + /> + ); + } + + /** + * Function which renders a section header component + * + * @param {Object} params + * @param {Object} params.section + * @param {String} params.section.title + * + * @return {Component} + */ + renderSectionHeader({section: {title}}) { + return ( + + + {title} + + + ); + } + + render() { + const sections = this.getSections(); + return ( + + {() => ( + <> + + + this.textInput = el} + style={[styles.textInput]} + value={this.state.searchValue} + onChangeText={(searchValue = '') => { + const {currencyOptions} = getCurrencyListForSections( + this.props.currencyList, + searchValue, + ); + this.setState({ + searchValue, + currencyData: currencyOptions, + }); + }} + placeholder="Search" + placeholderTextColor={themeColors.placeholderText} + /> + + + + [ + styles.button, + styles.buttonSuccess, + styles.w100, + hovered && styles.buttonSuccessHovered, + ]} + > + + Confirm + + + + + + )} + + ); + } +} + +IOUCurrencySelection.propTypes = propTypes; +IOUCurrencySelection.defaultProps = defaultProps; +IOUCurrencySelection.displayName = 'IOUModal'; + +export default withOnyx({currencyList: {key: ONYXKEYS.CURRENCY_LIST}})(IOUCurrencySelection); diff --git a/src/pages/iou/IOUModal.js b/src/pages/iou/IOUModal.js index 593addfda4700..048faba817a01 100644 --- a/src/pages/iou/IOUModal.js +++ b/src/pages/iou/IOUModal.js @@ -1,6 +1,7 @@ import React, {Component} from 'react'; -import {View, TouchableOpacity} from 'react-native'; +import {View, TouchableOpacity, Keyboard} from 'react-native'; import PropTypes from 'prop-types'; +import Onyx, {withOnyx} from 'react-native-onyx'; import IOUAmountPage from './steps/IOUAmountPage'; import IOUParticipantsPage from './steps/IOUParticipantsPage'; import IOUConfirmPage from './steps/IOUConfirmPage'; @@ -10,6 +11,7 @@ import Icon from '../../components/Icon'; import {getPreferredCurrency} from '../../libs/actions/IOU'; import {Close, BackArrow} from '../../components/Icon/Expensicons'; import Navigation from '../../libs/Navigation/Navigation'; +import ONYXKEYS from '../../ONYXKEYS'; /** * IOU modal for requesting money and splitting bills. @@ -17,10 +19,21 @@ import Navigation from '../../libs/Navigation/Navigation'; const propTypes = { // Is this new IOU for a single request or group bill split? hasMultipleParticipants: PropTypes.bool, + + // 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, + }), }; const defaultProps = { hasMultipleParticipants: false, + myPersonalDetails: {preferredCurrencyCode: 'USD', preferredCurrencySymbol: '$'}, }; // Determines type of step to display within Modal, value provides the title for that page. @@ -40,14 +53,20 @@ class IOUModal extends Component { this.navigateToPreviousStep = this.navigateToPreviousStep.bind(this); this.navigateToNextStep = this.navigateToNextStep.bind(this); this.updateAmount = this.updateAmount.bind(this); - this.currencySelected = this.currencySelected.bind(this); - + this.selectCurrency = this.selectCurrency.bind(this); + this.setCurrencyMode = this.setCurrencyMode.bind(this); + this.confirmCurrencySelection = this.confirmCurrencySelection.bind(this); this.addParticipants = this.addParticipants.bind(this); + this.state = { currentStepIndex: 0, participants: [], amount: '', - selectedCurrency: 'USD', + selectedCurrency: { + currencyCode: props.myPersonalDetails.preferredCurrencyCode, + currencySymbol: props.myPersonalDetails.preferredCurrencySymbol, + }, + currencySelectionMode: false, isAmountPageNextButtonDisabled: true, }; } @@ -63,8 +82,12 @@ class IOUModal extends Component { */ getTitleForStep() { + if (this.state.currencySelectionMode) { + return 'Select Currency'; + } if (this.state.currentStepIndex === 1) { - return `${this.props.hasMultipleParticipants ? 'Split' : 'Request'} $${this.state.amount}`; + return (`${this.props.hasMultipleParticipants ? 'Split' + : 'Request'} ${this.state.selectedCurrency.currencySymbol}${this.state.amount}`); } if (steps[this.state.currentStepIndex] === Steps.IOUAmount) { return this.props.hasMultipleParticipants ? 'Split Bill' : 'Request Money'; @@ -72,6 +95,16 @@ class IOUModal extends Component { return steps[this.state.currentStepIndex] || ''; } + /** + * Update the currency state + * + * @param {bool} isCurrencyModeOn + */ + setCurrencyMode(isCurrencyModeOn) { + Keyboard.dismiss(); + this.setState({currencySelectionMode: isCurrencyModeOn}); + } + /** * Navigate to the next IOU step if possible */ @@ -132,12 +165,24 @@ class IOUModal extends Component { /** * Update the currency state * - * @param {String} selectedCurrency + * @param {Object} selectedCurrency */ - currencySelected(selectedCurrency) { + selectCurrency(selectedCurrency) { this.setState({selectedCurrency}); } + /** + * Update the currency state + * + */ + confirmCurrencySelection() { + Onyx.merge(ONYXKEYS.MY_PERSONAL_DETAILS, { + preferredCurrencyCode: this.state.selectedCurrency.currencyCode, + preferredCurrencySymbol: this.state.selectedCurrency.currencySymbol, + }); + this.setState({currencySelectionMode: false}); + } + render() { const currentStep = steps[this.state.currentStepIndex]; return ( @@ -176,8 +221,11 @@ class IOUModal extends Component { @@ -206,4 +254,4 @@ IOUModal.propTypes = propTypes; IOUModal.defaultProps = defaultProps; IOUModal.displayName = 'IOUModal'; -export default IOUModal; +export default withOnyx({myPersonalDetails: {key: ONYXKEYS.MY_PERSONAL_DETAILS}})(IOUModal); diff --git a/src/pages/iou/steps/IOUAmountPage.js b/src/pages/iou/steps/IOUAmountPage.js index 4d0c2e99d67e6..015250fdae78c 100644 --- a/src/pages/iou/steps/IOUAmountPage.js +++ b/src/pages/iou/steps/IOUAmountPage.js @@ -13,6 +13,7 @@ import themeColors from '../../../styles/themes/default'; import BigNumberPad from '../../../components/BigNumberPad'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; import TextInputFocusable from '../../../components/TextInputFocusable'; +import IOUCurrencySelection from '../IOUCurrencySelection'; const propTypes = { // Callback to inform parent modal of success @@ -21,13 +22,21 @@ const propTypes = { // Callback to inform parent modal with key pressed numberPressed: PropTypes.func.isRequired, - // Currency selection will be implemented later - // eslint-disable-next-line react/no-unused-prop-types - currencySelected: PropTypes.func.isRequired, + // Callback that sets selectedCurrency in IOUModal + onCurrencySelected: PropTypes.func.isRequired, // User's currency preference selectedCurrency: PropTypes.string.isRequired, + // Whether or not currency selection mode is on + currencySelectionMode: PropTypes.bool, + + // Callback that sets currency in Onyx and dismisses the currency mode + onCurrencyConfirm: PropTypes.func, + + // Callback to set currency selection mode + setCurrencySelectionMode: PropTypes.func, + // Amount value entered by user amount: PropTypes.string.isRequired, @@ -49,7 +58,11 @@ const propTypes = { const defaultProps = { iou: {}, + currencySelectionMode: false, + setCurrencySelectionMode: null, + onCurrencyConfirm: null, }; + class IOUAmountPage extends React.Component { constructor(props) { super(props); @@ -65,7 +78,35 @@ class IOUAmountPage extends React.Component { } } + /** + * Returns the sections needed for the OptionsSelector + * + * @param {Boolean} maxParticipantsReached + * @returns {Array} + */ + getSections() { + const sections = []; + sections.push({ + title: undefined, + data: this.props.participants, + shouldShow: true, + indexOffset: 0, + }); + + return sections; + } + render() { + if (this.props.currencySelectionMode) { + return ( + + ); + } return ( {this.props.iou.loading && } @@ -77,9 +118,11 @@ class IOUAmountPage extends React.Component { styles.justifyContentCenter, ]} > - - {this.props.selectedCurrency} - + this.props.setCurrencySelectionMode(true)}> + + {this.props.selectedCurrency.currencySymbol} + + {this.props.isSmallScreenWidth ? {this.props.amount} : ( @@ -127,6 +170,7 @@ IOUAmountPage.displayName = 'IOUAmountPage'; IOUAmountPage.propTypes = propTypes; IOUAmountPage.defaultProps = defaultProps; -export default withWindowDimensions(withOnyx({ +export default withWindowDimensions( + withOnyx({ iou: {key: ONYXKEYS.IOU}, })(IOUAmountPage));