diff --git a/android/app/build.gradle b/android/app/build.gradle index 469b497bbc3d..d2389835ee46 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -90,8 +90,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001038002 - versionName "1.3.80-2" + versionCode 1001038100 + versionName "1.3.81-0" } flavorDimensions "default" diff --git a/docs/_sass/_search-bar.scss b/docs/_sass/_search-bar.scss index a5cc8ae2ff20..e9c56835af50 100644 --- a/docs/_sass/_search-bar.scss +++ b/docs/_sass/_search-bar.scss @@ -88,11 +88,14 @@ $color-gray-label: $color-gray-label; /* All gsc id & class are Google Search relate gcse_0 is the search bar parent & gcse_1 is the search result list parent */ #___gcse_0 { margin-left: 20px; + margin-top: -8px; } /* This input is in #___gcse_0 search bar */ input#gsc-i-id1.gsc-input { background-color: $color-appBG; + padding: 15px 0px 0px !important; + pointer-events: auto; color: #E7ECE9; font-family: "ExpensifyNeue", "Segoe UI Emoji", "Noto Color Emoji" !important; } @@ -102,23 +105,29 @@ input#gsc-i-id1.gsc-input { background-color: $color-appBG; border-bottom: $color-borders 2px solid; border-bottom-left-radius: 0px; - + pointer-events: none; + &:focus-within { border-bottom: $color-accent 2px solid; } } .gsc-input-box .gsib_a { - padding: 5px 9px 4px 0px; + padding: 0px 0px 4px 0px; } .search-icon { margin-left: auto; } +.gsst_b, .gsst_a { + padding: 0px !important; +} /* This is the close icon on search bar */ .gsib_b .gsst_a .gscb_a { color: $color-icons; + padding: 8px 6px 0px 6px !important; + pointer-events: auto; &:hover { color: $color-text; @@ -148,6 +157,7 @@ label.search-label { font-family: "ExpensifyNeue", "Segoe UI Emoji", "Noto Color Emoji"; transform: translateY(-50%); left: 20px; + pointer-events: none; color: $color-gray-label; transform-origin: left top; user-select: none; @@ -181,7 +191,6 @@ label.search-label { /* Change the Google Search Button icon into Expensify icon button */ .gsc-search-button.gsc-search-button-v2 { padding: 10px; - margin-top: -7px; margin-left: 15px; margin-right: 20px; border-radius: 25px; diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 5ed5b3345338..d64f54d4be85 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.80 + 1.3.81 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.3.80.2 + 1.3.81.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index f2162dbea995..41447e5ce1f2 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.3.80 + 1.3.81 CFBundleSignature ???? CFBundleVersion - 1.3.80.2 + 1.3.81.0 diff --git a/package-lock.json b/package-lock.json index 1681941f8aec..5d51d03f3b96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.3.80-2", + "version": "1.3.81-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.80-2", + "version": "1.3.81-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 5da7256344a9..18c2176282be 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.80-2", + "version": "1.3.81-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/CONST.ts b/src/CONST.ts index 9ce1152d5dcb..a11faa33323e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1263,7 +1263,7 @@ const CONST = { CARD_NUMBER: /^[0-9]{15,16}$/, CARD_SECURITY_CODE: /^[0-9]{3,4}$/, CARD_EXPIRATION_DATE: /^(0[1-9]|1[0-2])([^0-9])?([0-9]{4}|([0-9]{2}))$/, - ROOM_NAME: /^#[a-z0-9à-ÿ-]{1,80}$/, + ROOM_NAME: /^#[\p{Ll}0-9-]{1,80}$/u, // eslint-disable-next-line max-len, no-misleading-character-class EMOJIS: /[\p{Extended_Pictographic}](\u200D[\p{Extended_Pictographic}]|[\u{1F3FB}-\u{1F3FF}]|[\u{E0020}-\u{E007F}]|\uFE0F|\u20E3)*|[\u{1F1E6}-\u{1F1FF}]{2}|[#*0-9]\uFE0F?\u20E3/gu, diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 0a17d3a1d2f7..7b6335ccf984 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -291,6 +291,7 @@ const ONYXKEYS = { PRIVATE_NOTES_FORM: 'privateNotesForm', I_KNOW_A_TEACHER_FORM: 'iKnowTeacherForm', INTRO_SCHOOL_PRINCIPAL_FORM: 'introSchoolPrincipalForm', + REPORT_VIRTUAL_CARD_FRAUD: 'reportVirtualCardFraudForm', }, } as const; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 21aaa55f099b..2069f773075b 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -75,6 +75,10 @@ export default { route: '/settings/wallet/card/:domain', getRoute: (domain: string) => `/settings/wallet/card/${domain}`, }, + SETTINGS_REPORT_FRAUD: { + route: '/settings/wallet/cards/:domain/report-virtual-fraud', + getRoute: (domain: string) => `/settings/wallet/cards/${domain}/report-virtual-fraud`, + }, SETTINGS_ADD_DEBIT_CARD: 'settings/wallet/add-debit-card', SETTINGS_ADD_BANK_ACCOUNT: 'settings/wallet/add-bank-account', SETTINGS_ENABLE_PAYMENTS: 'settings/wallet/enable-payments', diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js index d5c753d83735..6c41290f1d17 100644 --- a/src/components/MenuItem.js +++ b/src/components/MenuItem.js @@ -24,7 +24,7 @@ import variables from '../styles/variables'; import * as Session from '../libs/actions/Session'; import Hoverable from './Hoverable'; import useWindowDimensions from '../hooks/useWindowDimensions'; -import MenuItemRenderHTMLTitle from './MenuItemRenderHTMLTitle'; +import RenderHTML from './RenderHTML'; const propTypes = menuItemPropTypes; @@ -252,7 +252,9 @@ const MenuItem = React.forwardRef((props, ref) => { )} {Boolean(props.title) && (Boolean(props.shouldRenderAsHTML) || (Boolean(props.shouldParseTitle) && Boolean(html.length))) && ( - + + + )} {!props.shouldRenderAsHTML && !props.shouldParseTitle && Boolean(props.title) && ( - - - ); -} - -MenuItemRenderHTMLTitle.propTypes = propTypes; -MenuItemRenderHTMLTitle.defaultProps = defaultProps; -MenuItemRenderHTMLTitle.displayName = 'MenuItemRenderHTMLTitle'; - -export default MenuItemRenderHTMLTitle; diff --git a/src/components/MenuItemRenderHTMLTitle/index.native.js b/src/components/MenuItemRenderHTMLTitle/index.native.js deleted file mode 100644 index b3dff8d77eff..000000000000 --- a/src/components/MenuItemRenderHTMLTitle/index.native.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import RenderHTML from '../RenderHTML'; -import menuItemRenderHTMLTitlePropTypes from './propTypes'; - -const propTypes = menuItemRenderHTMLTitlePropTypes; - -const defaultProps = {}; - -function MenuItemRenderHTMLTitle(props) { - return ; -} - -MenuItemRenderHTMLTitle.propTypes = propTypes; -MenuItemRenderHTMLTitle.defaultProps = defaultProps; -MenuItemRenderHTMLTitle.displayName = 'MenuItemRenderHTMLTitle'; - -export default MenuItemRenderHTMLTitle; diff --git a/src/components/MenuItemRenderHTMLTitle/propTypes.js b/src/components/MenuItemRenderHTMLTitle/propTypes.js deleted file mode 100644 index 68e279eb28c3..000000000000 --- a/src/components/MenuItemRenderHTMLTitle/propTypes.js +++ /dev/null @@ -1,8 +0,0 @@ -import PropTypes from 'prop-types'; - -const propTypes = { - /** Processed title to display for the MenuItem */ - title: PropTypes.string.isRequired, -}; - -export default propTypes; diff --git a/src/components/PopoverWithoutOverlay/index.js b/src/components/PopoverWithoutOverlay/index.js index d35637958f1d..3b194ad4b9cf 100644 --- a/src/components/PopoverWithoutOverlay/index.js +++ b/src/components/PopoverWithoutOverlay/index.js @@ -8,7 +8,6 @@ import styles from '../../styles/styles'; import * as StyleUtils from '../../styles/StyleUtils'; import getModalStyles from '../../styles/getModalStyles'; import withWindowDimensions from '../withWindowDimensions'; -import usePrevious from '../../hooks/usePrevious'; function Popover(props) { const {onOpen, close} = React.useContext(PopoverContext); @@ -25,8 +24,6 @@ function Popover(props) { props.outerStyle, ); - const prevIsVisible = usePrevious(props.isVisible); - React.useEffect(() => { if (props.isVisible) { props.onModalShow(); @@ -43,7 +40,7 @@ function Popover(props) { Modal.willAlertModalBecomeVisible(props.isVisible); // We prevent setting closeModal function to null when the component is invisible the first time it is rendered - if (prevIsVisible === props.isVisible && (!firstRenderRef.current || !props.isVisible)) { + if (!firstRenderRef.current || !props.isVisible) { firstRenderRef.current = false; return; } @@ -52,7 +49,7 @@ function Popover(props) { // We want this effect to run strictly ONLY when isVisible prop changes // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.isVisible, prevIsVisible]); + }, [props.isVisible]); if (!props.isVisible) { return null; diff --git a/src/components/QRShare/index.js b/src/components/QRShare/index.js index d96024ad1046..837adcac8efe 100644 --- a/src/components/QRShare/index.js +++ b/src/components/QRShare/index.js @@ -76,7 +76,6 @@ class QRShare extends Component { {!_.isEmpty(this.props.subtitle) && ( require('../../../pages/settings/Profile/LoungeAccessPage').default, Settings_Wallet: () => require('../../../pages/settings/Wallet/WalletPage').default, Settings_Wallet_DomainCards: () => require('../../../pages/settings/Wallet/ExpensifyCardPage').default, + Settings_Wallet_ReportVirtualCardFraud: () => require('../../../pages/settings/Wallet/ReportVirtualCardFraudPage').default, Settings_Wallet_Card_Activate: () => require('../../../pages/settings/Wallet/ActivatePhysicalCardPage').default, Settings_Wallet_Transfer_Balance: () => require('../../../pages/settings/Wallet/TransferBalancePage').default, Settings_Wallet_Choose_Transfer_Account: () => require('../../../pages/settings/Wallet/ChooseTransferAccountPage').default, diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index 821b73268d0d..5616e8d63797 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -73,6 +73,10 @@ export default { path: ROUTES.SETTINGS_WALLET_DOMAINCARDS.route, exact: true, }, + Settings_Wallet_ReportVirtualCardFraud: { + path: ROUTES.SETTINGS_REPORT_FRAUD.route, + exact: true, + }, Settings_Wallet_EnablePayments: { path: ROUTES.SETTINGS_ENABLE_PAYMENTS, exact: true, diff --git a/src/libs/actions/Card.js b/src/libs/actions/Card.js index abfd5a6bba98..a060c1bc67fa 100644 --- a/src/libs/actions/Card.js +++ b/src/libs/actions/Card.js @@ -2,6 +2,47 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../ONYXKEYS'; import * as API from '../API'; +/** + * @param {Number} cardID + */ +function reportVirtualExpensifyCardFraud(cardID) { + API.write( + 'ReportVirtualExpensifyCardFraud', + { + cardID, + }, + { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, + value: { + isLoading: true, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, + value: { + isLoading: false, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, + value: { + isLoading: false, + }, + }, + ], + }, + ); +} + /** * Activates the physical Expensify card based on the last four digits of the card number * @@ -60,4 +101,4 @@ function clearCardListErrors(cardID) { Onyx.merge(ONYXKEYS.CARD_LIST, {[cardID]: {errors: null, isLoading: false}}); } -export {activatePhysicalExpensifyCard, clearCardListErrors}; +export {reportVirtualExpensifyCardFraud, activatePhysicalExpensifyCard, clearCardListErrors}; diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index 5cda218e82be..80fd1d39239d 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -185,16 +185,16 @@ function FloatingActionButtonAndPopover(props) { text: props.translate('sidebarScreen.fabNewChat'), onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.NEW)), }, - { - icon: Expensicons.Send, - text: props.translate('iou.sendMoney'), - onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.MONEY_REQUEST_TYPE.SEND)), - }, { icon: Expensicons.MoneyCircle, text: props.translate('iou.requestMoney'), onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.MONEY_REQUEST_TYPE.REQUEST)), }, + { + icon: Expensicons.Send, + text: props.translate('iou.sendMoney'), + onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.MONEY_REQUEST_TYPE.SEND)), + }, ...(Permissions.canUseTasks(props.betas) ? [ { @@ -204,6 +204,11 @@ function FloatingActionButtonAndPopover(props) { }, ] : []), + { + icon: Expensicons.Heart, + text: props.translate('sidebarScreen.saveTheWorld'), + onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.TEACHERS_UNITE)), + }, ...(!props.isLoading && !Policy.hasActiveFreePolicy(props.allPolicies) ? [ { diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 19d589acb421..cfbd26133ced 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -14,6 +14,7 @@ import useLocalize from '../../../hooks/useLocalize'; import * as CurrencyUtils from '../../../libs/CurrencyUtils'; import Navigation from '../../../libs/Navigation/Navigation'; import styles from '../../../styles/styles'; +import * as Expensicons from '../../../components/Icon/Expensicons'; import * as CardUtils from '../../../libs/CardUtils'; import Button from '../../../components/Button'; import CardDetails from './WalletPage/CardDetails'; @@ -108,6 +109,13 @@ function ExpensifyCardPage({ } /> )} + Navigation.navigate(ROUTES.SETTINGS_REPORT_FRAUD.getRoute(domain))} + /> )} {!_.isEmpty(physicalCard) && ( @@ -115,7 +123,7 @@ function ExpensifyCardPage({ description={translate('cardPage.physicalCardNumber')} title={CardUtils.maskCard(physicalCard.lastFourPAN)} interactive={false} - titleStyle={styles.walletCardNumber} + titleStyle={styles.walletCardMenuItem} /> )} diff --git a/src/pages/settings/Wallet/ReportVirtualCardFraudPage.js b/src/pages/settings/Wallet/ReportVirtualCardFraudPage.js new file mode 100644 index 000000000000..2652494aa1c7 --- /dev/null +++ b/src/pages/settings/Wallet/ReportVirtualCardFraudPage.js @@ -0,0 +1,104 @@ +import React, {useEffect} from 'react'; +import _ from 'underscore'; +import {View} from 'react-native'; +import PropTypes from 'prop-types'; +import {withOnyx} from 'react-native-onyx'; +import ROUTES from '../../../ROUTES'; +import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; +import ScreenWrapper from '../../../components/ScreenWrapper'; +import Navigation from '../../../libs/Navigation/Navigation'; +import styles from '../../../styles/styles'; +import Text from '../../../components/Text'; +import useLocalize from '../../../hooks/useLocalize'; +import * as Card from '../../../libs/actions/Card'; +import assignedCardPropTypes from './assignedCardPropTypes'; +import * as CardUtils from '../../../libs/CardUtils'; +import ONYXKEYS from '../../../ONYXKEYS'; +import NotFoundPage from '../../ErrorPage/NotFoundPage'; +import usePrevious from '../../../hooks/usePrevious'; +import FormAlertWithSubmitButton from '../../../components/FormAlertWithSubmitButton'; +import * as ErrorUtils from '../../../libs/ErrorUtils'; + +const propTypes = { + /* Onyx Props */ + formData: PropTypes.shape({ + isLoading: PropTypes.bool, + }), + cardList: PropTypes.objectOf(assignedCardPropTypes), + /** The parameters needed to authenticate with a short-lived token are in the URL */ + route: PropTypes.shape({ + /** Each parameter passed via the URL */ + params: PropTypes.shape({ + /** Domain string */ + domain: PropTypes.string, + }), + }).isRequired, +}; + +const defaultProps = { + cardList: {}, + formData: {}, +}; + +function ReportVirtualCardFraudPage({ + route: { + params: {domain}, + }, + cardList, + formData, +}) { + const {translate} = useLocalize(); + + const domainCards = CardUtils.getDomainCards(cardList)[domain]; + const virtualCard = _.find(domainCards, (card) => card.isVirtual) || {}; + const virtualCardError = ErrorUtils.getLatestErrorMessage(virtualCard) || ''; + + const prevIsLoading = usePrevious(formData.isLoading); + + useEffect(() => { + if (!prevIsLoading || formData.isLoading) { + return; + } + if (!_.isEmpty(virtualCard.errors)) { + return; + } + + Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARDS.getRoute(domain)); + }, [domain, formData.isLoading, prevIsLoading, virtualCard.errors]); + + if (_.isEmpty(virtualCard)) { + return ; + } + + return ( + + Navigation.goBack(ROUTES.SETTINGS_WALLET_DOMAINCARDS.getRoute(domain))} + /> + + {translate('reportFraudPage.description')} + Card.reportVirtualExpensifyCardFraud(virtualCard.cardID)} + message={virtualCardError} + isLoading={formData.isLoading} + buttonText={translate('reportFraudPage.deactivateCard')} + /> + + + ); +} + +ReportVirtualCardFraudPage.propTypes = propTypes; +ReportVirtualCardFraudPage.defaultProps = defaultProps; +ReportVirtualCardFraudPage.displayName = 'ReportVirtualCardFraudPage'; + +export default withOnyx({ + cardList: { + key: ONYXKEYS.CARD_LIST, + }, + formData: { + key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD, + }, +})(ReportVirtualCardFraudPage); diff --git a/src/styles/styles.js b/src/styles/styles.js index d4aacfff96aa..7cb4dab6e0d7 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -3770,7 +3770,7 @@ const styles = (theme) => ({ overflow: 'hidden', }, - walletCardNumber: { + walletCardMenuItem: { color: theme.text, fontSize: variables.fontSizeNormal, }, diff --git a/tests/unit/ValidationUtilsTest.js b/tests/unit/ValidationUtilsTest.js index 003e0ab75afe..a9e0b1b61128 100644 --- a/tests/unit/ValidationUtilsTest.js +++ b/tests/unit/ValidationUtilsTest.js @@ -253,6 +253,14 @@ describe('ValidationUtils', () => { test('room name with spanish Accented letters and dashes', () => { expect(ValidationUtils.isValidRoomName('#sala-de-opinión')).toBe(true); }); + + test('room name with division sign (÷)', () => { + expect(ValidationUtils.isValidRoomName('#room-name-with-÷-sign')).toBe(false); + }); + + test('room name with Greek alphabets and Cyrillic alphabets', () => { + expect(ValidationUtils.isValidRoomName('#σοβαρός-серьезный')).toBe(true); + }); }); describe('isValidWebsite', () => {