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', () => {