diff --git a/.github/ISSUE_TEMPLATE/Standard.md b/.github/ISSUE_TEMPLATE/Standard.md
index 123d0143e9af..9ae9bbe077d0 100644
--- a/.github/ISSUE_TEMPLATE/Standard.md
+++ b/.github/ISSUE_TEMPLATE/Standard.md
@@ -7,15 +7,15 @@ labels: AutoAssignerTriage
If you haven’t already, check out our [contributing guidelines](https://github.com/Expensify/ReactNativeChat/blob/main/CONTRIBUTING.md) for onboarding and email contributors@expensify.com to request to join our Slack channel!
___
+## Action Performed:
+Break down in numbered steps
+
## Expected Result:
Describe what you think should've happened
## Actual Result:
Describe what actually happened
-## Action Performed:
-Break down in numbered steps
-
## Workaround:
Can the user still use Expensify without this being fixed? Have you informed them of the workaround?
diff --git a/README.md b/README.md
index 545f4055e356..a02c8008f6e9 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,7 @@
* [Debugging](#debugging)
* [Structure of the app](#structure-of-the-app)
* [Philosophy](#Philosophy)
+* [Internationalization](#Internationalization)
* [Deploying](#deploying)
#### Additional Reading
@@ -236,6 +237,32 @@ This application is built with the following principles.
----
+# Internationalization
+This application is built with Internationalization (I18n) / Localization (L10n) support, so it's important to always
+localize the following types of data when presented to the user (even accessibility texts that are not rendered):
+
+- Texts: See [translate method](https://github.com/Expensify/Expensify.cash/blob/655ba416d552d5c88e57977a6e0165fb7eb7ab58/src/libs/translate.js#L15)
+- Date/time: see [DateUtils](https://github.com/Expensify/Expensify.cash/blob/f579946fbfbdc62acc5bd281dc75cabb803d9af0/src/libs/DateUtils.js)
+- Numbers and amounts: see [numberFormat](https://github.com/Expensify/Expensify.cash/tree/965f92fc2a5a2a0d01e6114bf5aa8755b9d9fd1a/src/libs/numberFormat)
+- Phones: see [LocalPhoneNumber](https://github.com/Expensify/Expensify.cash/blob/bdfbafe18ee2d60f766c697744f23fad64b62cad/src/libs/LocalePhoneNumber.js#L51-L52)
+
+In most cases, you will be needing to localize data used in a component, if that's the case, there's a HOC [withLocalize](https://github.com/Expensify/Expensify.cash/blob/37465dbd07da1feab8347835d82ed3d2302cde4c/src/components/withLocalize.js).
+It will abstract most of the logic you need (mostly subscribe to the [PREFERRED_LOCALE](https://github.com/Expensify/Expensify.cash/blob/6cf1a56df670a11bf61aa67eeb64c1f87161dea1/src/ONYXKEYS.js#L88) Onyx key)
+and is the preferred way of localizing things inside components.
+
+Some pointers:
+
+- All translations are stored in language files in [src/languages](https://github.com/Expensify/Expensify.cash/tree/b114bc86ff38e3feca764e75b3f5bf4f60fcd6fe/src/languages).
+- We try to group translations by their pages/components
+- A common rule of thumb is to move a common word/phrase to be shared when it's in 3 places
+- Always prefer longer and more complex strings in the translation files. For example
+ if you need to generate the text `User has sent $20.00 to you on Oct 25th at 10:05am`, add just one
+ key to the translation file and use the arrow function version, like so:
+ `nameOfTheKey: ({amount, dateTime}) => "User has sent " + amount + " to you on " + dateTime,`.
+ This is because the order of the phrases might vary from one language to another.
+
+----
+
# Deploying
## QA and deploy cycles
We utilize a CI/CD deployment system built using [GitHub Actions](https://github.com/features/actions) to ensure that new code is automatically deployed to our users as fast as possible. As part of this process, all code is first deployed to our staging environments, where it undergoes quality assurance (QA) testing before it is deployed to production. Typically, pull requests are deployed to staging immediately after they are merged.
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 5f58d28bd8b7..9c5ea243bd9c 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -148,8 +148,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001006804
- versionName "1.0.68-4"
+ versionCode 1001006805
+ versionName "1.0.68-5"
}
splits {
abi {
diff --git a/assets/images/workspace-default-avatar.svg b/assets/images/workspace-default-avatar.svg
new file mode 100644
index 000000000000..6b17c170fdbe
--- /dev/null
+++ b/assets/images/workspace-default-avatar.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ios/ExpensifyCash/Info.plist b/ios/ExpensifyCash/Info.plist
index 6b15df09344f..6ff66da3ea69 100644
--- a/ios/ExpensifyCash/Info.plist
+++ b/ios/ExpensifyCash/Info.plist
@@ -30,7 +30,7 @@
CFBundleVersion
- 1.0.68.4
+ 1.0.68.5
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/ExpensifyCashTests/Info.plist b/ios/ExpensifyCashTests/Info.plist
index f7f50226c806..497c59a88ff5 100644
--- a/ios/ExpensifyCashTests/Info.plist
+++ b/ios/ExpensifyCashTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.0.68.4
+ 1.0.68.5
diff --git a/package-lock.json b/package-lock.json
index 4797f907e560..46a13d03a59e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "expensify.cash",
- "version": "1.0.68-4",
+ "version": "1.0.68-5",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/package.json b/package.json
index 741cdffa9b48..816ec1e5cd33 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "expensify.cash",
- "version": "1.0.68-4",
+ "version": "1.0.68-5",
"author": "Expensify, Inc.",
"homepage": "https://expensify.cash",
"description": "Expensify.cash is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/src/App.js b/src/App.js
index 58845d56e429..c69e6da22cb8 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,12 +1,15 @@
import React from 'react';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import CustomStatusBar from './components/CustomStatusBar';
+import ErrorBoundary from './components/ErrorBoundary';
import Expensify from './Expensify';
const App = () => (
-
+
+
+
);
diff --git a/src/CONST.js b/src/CONST.js
index 67b9010b927c..f786194b6168 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -22,6 +22,7 @@ const CONST = {
IOU: 'IOU',
PAY_WITH_EXPENSIFY: 'payWithExpensify',
FREE_PLAN: 'freePlan',
+ DEFAULT_ROOMS: 'defaultRooms',
},
BUTTON_STATES: {
DEFAULT: 'default',
@@ -80,6 +81,7 @@ const CONST = {
MODAL_TYPE: {
CONFIRM: 'confirm',
CENTERED: 'centered',
+ CENTERED_UNSWIPEABLE: 'centered_unswipeable',
BOTTOM_DOCKED: 'bottom_docked',
POPOVER: 'popover',
RIGHT_DOCKED: 'right_docked',
diff --git a/src/Expensify.js b/src/Expensify.js
index 431db2e8d9a1..0d26f00c96ec 100644
--- a/src/Expensify.js
+++ b/src/Expensify.js
@@ -1,10 +1,11 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {PureComponent} from 'react';
-import {View, StatusBar, AppState} from 'react-native';
+import {View, AppState} from 'react-native';
import Onyx, {withOnyx} from 'react-native-onyx';
import BootSplash from './libs/BootSplash';
+import StatusBar from './libs/StatusBar';
import listenToStorageEvents from './libs/listenToStorageEvents';
import * as ActiveClientManager from './libs/ActiveClientManager';
import ONYXKEYS from './ONYXKEYS';
@@ -18,6 +19,8 @@ import UpdateAppModal from './components/UpdateAppModal';
import Visibility from './libs/Visibility';
import GrowlNotification from './components/GrowlNotification';
import {growlRef} from './libs/Growl';
+import Navigation from './libs/Navigation/Navigation';
+import ROUTES from './ROUTES';
// Initialize the store when the app loads for the first time
Onyx.init({
@@ -54,6 +57,9 @@ const propTypes = {
/** Currently logged in user accountID */
accountID: PropTypes.number,
+
+ /** Should app immediately redirect to new workspace route once authenticated */
+ redirectToWorkspaceNewAfterSignIn: PropTypes.bool,
}),
/** Whether a new update is available and ready to install. */
@@ -67,6 +73,7 @@ const defaultProps = {
session: {
authToken: null,
accountID: null,
+ redirectToWorkspaceNewAfterSignIn: false,
},
updateAvailable: false,
initialReportDataLoaded: false,
@@ -113,6 +120,9 @@ class Expensify extends PureComponent {
const previousAuthToken = lodashGet(prevProps, 'session.authToken', null);
if (this.getAuthToken() && !previousAuthToken) {
BootSplash.show({fade: true});
+ if (lodashGet(this.props, 'session.redirectToWorkspaceNewAfterSignIn', false)) {
+ Navigation.navigate(ROUTES.WORKSPACE_NEW);
+ }
}
if (this.getAuthToken() && this.props.initialReportDataLoaded) {
diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js
index d7fcd65074f3..dda30feab524 100755
--- a/src/ONYXKEYS.js
+++ b/src/ONYXKEYS.js
@@ -21,6 +21,9 @@ export default {
// What the active route is for our navigator. Global route that determines what views to display.
CURRENT_URL: 'currentURL',
+ // Stores current date
+ CURRENT_DATE: 'currentDate',
+
// Currently viewed reportID
CURRENTLY_VIEWED_REPORTID: 'currentlyViewedReportID',
diff --git a/src/ROUTES.js b/src/ROUTES.js
index 8fbb80f1853c..b46703870e4a 100644
--- a/src/ROUTES.js
+++ b/src/ROUTES.js
@@ -48,6 +48,7 @@ export default {
VALIDATE_LOGIN: 'v',
VALIDATE_LOGIN_WITH_VALIDATE_CODE: 'v/:accountID/:validateCode',
ENABLE_PAYMENTS: 'enable-payments',
+ WORKSPACE_NEW: 'workspace/new',
/**
* @param {String} route
diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index f42cddd6a57d..599456e55442 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -1,6 +1,7 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {View} from 'react-native';
+import Str from 'expensify-common/lib/str';
import {withOnyx} from 'react-native-onyx';
import CONST from '../CONST';
import Modal from './Modal';
@@ -97,10 +98,17 @@ class AttachmentModal extends PureComponent {
const attachmentViewStyles = this.props.isSmallScreenWidth
? [styles.imageModalImageCenterContainer]
: [styles.imageModalImageCenterContainer, styles.p5];
+
+ // If our attachment is a PDF, make the Modal unswipeable
+ const modalType = (this.state.sourceURL
+ && (Str.isPDF(this.state.sourceURL) || (this.state.file
+ && Str.isPDF(this.state.file.name || this.props.translate('attachmentView.unknownFilename')))))
+ ? CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE
+ : CONST.MODAL.MODAL_TYPE.CENTERED;
return (
<>
this.setState({isModalOpen: false})}
isVisible={this.state.isModalOpen}
diff --git a/src/components/Button.js b/src/components/Button.js
index a5854151e748..c170e64eb3a1 100644
--- a/src/components/Button.js
+++ b/src/components/Button.js
@@ -10,7 +10,7 @@ import OpacityView from './OpacityView';
const propTypes = {
/** The text for the button label */
- text: PropTypes.string.isRequired,
+ text: PropTypes.string,
/** Indicates whether the button should be disabled and in the loading state */
isLoading: PropTypes.bool,
@@ -44,6 +44,7 @@ const propTypes = {
};
const defaultProps = {
+ text: '',
isLoading: false,
isDisabled: false,
onPress: () => {},
diff --git a/src/components/ErrorBoundary/BaseErrorBoundary.js b/src/components/ErrorBoundary/BaseErrorBoundary.js
new file mode 100644
index 000000000000..aee0f94a5665
--- /dev/null
+++ b/src/components/ErrorBoundary/BaseErrorBoundary.js
@@ -0,0 +1,52 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+const propTypes = {
+ /* A message posted to `logError` (along with error data) when this component intercepts an error */
+ errorMessage: PropTypes.string.isRequired,
+
+ /* A function to perform the actual logging since different platforms support different tools */
+ logError: PropTypes.func,
+
+ /* Actual content wrapped by this error boundary */
+ children: PropTypes.node.isRequired,
+};
+
+const defaultProps = {
+ logError: () => {},
+};
+
+/**
+ * This component captures an error in the child component tree and logs it to the server
+ * It can be used to wrap the entire app as well as to wrap specific parts for more granularity
+ * @see {@link https://reactjs.org/docs/error-boundaries.html#where-to-place-error-boundaries}
+ */
+class BaseErrorBoundary extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {hasError: false};
+ }
+
+ static getDerivedStateFromError() {
+ // Update state so the next render will show the fallback UI.
+ return {hasError: true};
+ }
+
+ componentDidCatch(error, errorInfo) {
+ this.props.logError(this.props.errorMessage, error, errorInfo);
+ }
+
+ render() {
+ if (this.state.hasError) {
+ // For the moment we've decided not to render any fallback UI
+ return null;
+ }
+
+ return this.props.children;
+ }
+}
+
+BaseErrorBoundary.propTypes = propTypes;
+BaseErrorBoundary.defaultProps = defaultProps;
+
+export default BaseErrorBoundary;
diff --git a/src/components/ErrorBoundary/index.js b/src/components/ErrorBoundary/index.js
new file mode 100644
index 000000000000..7e8fdfdc2131
--- /dev/null
+++ b/src/components/ErrorBoundary/index.js
@@ -0,0 +1,9 @@
+import BaseErrorBoundary from './BaseErrorBoundary';
+import Log from '../../libs/Log';
+
+BaseErrorBoundary.defaultProps.logError = (errorMessage, error, errorInfo) => {
+ // Log the error to the server
+ Log.alert(errorMessage, 0, {error: error.message, errorInfo}, false);
+};
+
+export default BaseErrorBoundary;
diff --git a/src/components/ErrorBoundary/index.native.js b/src/components/ErrorBoundary/index.native.js
new file mode 100644
index 000000000000..d320b46984e0
--- /dev/null
+++ b/src/components/ErrorBoundary/index.native.js
@@ -0,0 +1,16 @@
+import crashlytics from '@react-native-firebase/crashlytics';
+
+import BaseErrorBoundary from './BaseErrorBoundary';
+import Log from '../../libs/Log';
+
+BaseErrorBoundary.defaultProps.logError = (errorMessage, error, errorInfo) => {
+ // Log the error to the server
+ Log.alert(errorMessage, 0, {error: error.message, errorInfo}, false);
+
+ /* On native we also log the error to crashlytics
+ * Since the error was handled we need to manually tell crashlytics about it */
+ crashlytics().log(`errorInfo: ${JSON.stringify(errorInfo)}`);
+ crashlytics().recordError(error);
+};
+
+export default BaseErrorBoundary;
diff --git a/src/components/FixedFooter.js b/src/components/FixedFooter.js
new file mode 100644
index 000000000000..8c4ef0a897fa
--- /dev/null
+++ b/src/components/FixedFooter.js
@@ -0,0 +1,27 @@
+import React from 'react';
+import {View} from 'react-native';
+import PropTypes from 'prop-types';
+import styles from '../styles/styles';
+
+const propTypes = {
+ /** Children to wrap in FixedFooter. */
+ children: PropTypes.node.isRequired,
+
+ /** Styles to be assigned to Container */
+ style: PropTypes.arrayOf(PropTypes.object),
+};
+
+const defaultProps = {
+ style: [],
+};
+
+const FixedFooter = props => (
+
+ {props.children}
+
+);
+
+FixedFooter.propTypes = propTypes;
+FixedFooter.defaultProps = defaultProps;
+FixedFooter.displayName = 'FixedFooter';
+export default FixedFooter;
diff --git a/src/components/IOUConfirmationList.js b/src/components/IOUConfirmationList.js
index 0dcecf63a326..3cec85b899a5 100755
--- a/src/components/IOUConfirmationList.js
+++ b/src/components/IOUConfirmationList.js
@@ -1,7 +1,7 @@
import React, {Component} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
-import {TextInput} from 'react-native-gesture-handler';
+import {ScrollView, TextInput} from 'react-native-gesture-handler';
import {withOnyx} from 'react-native-onyx';
import {withSafeAreaInsets} from 'react-native-safe-area-context';
import styles from '../styles/styles';
@@ -18,6 +18,7 @@ import withLocalize, {withLocalizePropTypes} from './withLocalize';
import SafeAreaInsetPropTypes from '../pages/SafeAreaInsetPropTypes';
import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions';
import compose from '../libs/compose';
+import FixedFooter from './FixedFooter';
const propTypes = {
/** Callback to inform parent modal of success */
@@ -241,8 +242,8 @@ class IOUConfirmationList extends Component {
},
);
return (
-
-
+ <>
+
-
-
- {this.props.translate('iOUConfirmationList.whatsItFor')}
-
-
-
+
+ {this.props.translate('iOUConfirmationList.whatsItFor')}
+
+
-
-
+
+
-
+
+ >
);
}
}
diff --git a/src/components/Modal/BaseModal.js b/src/components/Modal/BaseModal.js
index f0b5fb6a1a7a..faf5a4ecb65e 100644
--- a/src/components/Modal/BaseModal.js
+++ b/src/components/Modal/BaseModal.js
@@ -28,7 +28,9 @@ class BaseModal extends PureComponent {
*/
hideModalAndRemoveEventListeners() {
this.unsubscribeFromKeyEvents();
- setModalVisibility(false);
+ if (this.props.shouldSetModalVisibility) {
+ setModalVisibility(false);
+ }
this.props.onModalHide();
}
@@ -79,9 +81,12 @@ class BaseModal extends PureComponent {
onBackButtonPress={this.props.onClose}
onModalShow={() => {
this.subscribeToKeyEvents();
- setModalVisibility(true);
+ if (this.props.shouldSetModalVisibility) {
+ setModalVisibility(true);
+ }
this.props.onModalShow();
}}
+ propagateSwipe={this.props.propagateSwipe}
onModalHide={this.hideModalAndRemoveEventListeners}
onSwipeComplete={this.props.onClose}
swipeDirection={swipeDirection}
diff --git a/src/components/Modal/ModalPropTypes.js b/src/components/Modal/ModalPropTypes.js
index 6e6f7dd850d7..8da6403308a3 100644
--- a/src/components/Modal/ModalPropTypes.js
+++ b/src/components/Modal/ModalPropTypes.js
@@ -4,6 +4,9 @@ import CONST from '../../CONST';
import {windowDimensionsPropTypes} from '../withWindowDimensions';
const propTypes = {
+ /** Should we announce the Modal visibility changes? */
+ shouldSetModalVisibility: PropTypes.bool,
+
/** Callback method fired when the user requests to close the modal */
onClose: PropTypes.func.isRequired,
@@ -49,6 +52,7 @@ const propTypes = {
};
const defaultProps = {
+ shouldSetModalVisibility: true,
onSubmit: null,
type: '',
onModalHide: () => {},
diff --git a/src/components/Popover/index.js b/src/components/Popover/index.js
index 9879d1d79a5f..bfb412f53a2d 100644
--- a/src/components/Popover/index.js
+++ b/src/components/Popover/index.js
@@ -11,7 +11,7 @@ import withWindowDimensions from '../withWindowDimensions';
const Popover = props => (
(
-
-);
-
-Popover.propTypes = propTypes;
-Popover.defaultProps = defaultProps;
-Popover.displayName = 'Popover';
-
-export default withWindowDimensions(Popover);
diff --git a/src/languages/en.js b/src/languages/en.js
index a9d4d55bc293..14029b389eeb 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -256,7 +256,10 @@ export default {
setPasswordPage: {
passwordCannotBeBlank: 'Password cannot be blank',
enterPassword: 'Enter a password',
+ confirmNewPassword: 'Confirm the password',
setPassword: 'Set Password',
+ passwordsDontMatch: 'Passwords must match',
+ newPasswordPrompt: 'Your password must have at least 8 characters,\n1 capital letter, 1 lowercase letter, 1 number.',
},
bankAccount: {
accountNumber: 'Account Number',
@@ -332,4 +335,15 @@ export default {
session: {
offlineMessage: 'Looks like you\'re not connected to internet. Can you check your connection and try again?',
},
+ workspace: {
+ new: {
+ welcome: 'Welcome',
+ chooseAName: 'Choose a name',
+ helpText: 'Need help getting setup? Request a call below and we’ll have someone reach out to you.',
+ getStarted: 'Get started!',
+ editPhoto: 'Edit Photo',
+ uploadPhoto: 'Upload Photo',
+ requestCall: 'Request a call',
+ },
+ },
};
diff --git a/src/languages/es.js b/src/languages/es.js
index 894d36489822..a49bb131d0ad 100644
--- a/src/languages/es.js
+++ b/src/languages/es.js
@@ -289,4 +289,15 @@ export default {
session: {
offlineMessage: 'Parece que no estás conectado a internet. Comprueba tu conexión e inténtalo de nuevo.',
},
+ workspace: {
+ new: {
+ welcome: 'Bienvenido/a',
+ chooseAName: 'Elige un nombre',
+ helpText: '¿Necesitas ayuda con la configuración? Pídenos una llamada y una persona de nuestro equipo te ayudará.',
+ getStarted: '¡Empezar!',
+ uploadPhoto: 'Subir Foto',
+ editPhoto: 'Editar Foto',
+ requestCall: 'Concertar una llamada',
+ },
+ },
};
diff --git a/src/libs/API.js b/src/libs/API.js
index 72acc4efe6f8..75b37492e726 100644
--- a/src/libs/API.js
+++ b/src/libs/API.js
@@ -658,6 +658,21 @@ function User_GetBetas() {
return Network.post('User_GetBetas');
}
+/**
+ * @param {Object} parameters
+ * @param {String} parameters.email
+ * @param {Boolean} [parameters.requireCertainty]
+ * @returns {Promise}
+ */
+function User_IsFromPublicDomain(parameters) {
+ const commandName = 'User_IsFromPublicDomain';
+ requireParameters(['email'], parameters, commandName);
+ return Network.post(commandName, {
+ ...{requireCertainty: true},
+ ...parameters,
+ });
+}
+
/**
* @param {Object} parameters
* @param {String} parameters.email
@@ -880,6 +895,7 @@ export {
UpdateAccount,
User_SignUp,
User_GetBetas,
+ User_IsFromPublicDomain,
User_ReopenAccount,
User_SecondaryLogin_Send,
User_UploadAvatar,
diff --git a/src/libs/DateUtils.js b/src/libs/DateUtils.js
index d4fb2008ae41..84b44f501a15 100644
--- a/src/libs/DateUtils.js
+++ b/src/libs/DateUtils.js
@@ -88,12 +88,31 @@ function timestampToRelative(locale, timestamp) {
return moment(date).fromNow();
}
+/**
+ * A throttled version of a function that updates the current date in Onyx store
+ */
+const updateCurrentDate = _.throttle(() => {
+ const currentDate = moment().format('YYYY-MM-DD');
+ Onyx.set(ONYXKEYS.CURRENT_DATE, currentDate);
+}, 1000 * 60 * 60 * 3); // 3 hours
+
+/**
+ * Initialises the event listeners that trigger the current date update
+ */
+function startCurrentDateUpdater() {
+ const trackedEvents = ['mousemove', 'touchstart', 'keydown', 'scroll'];
+ trackedEvents.forEach((eventName) => {
+ document.addEventListener(eventName, updateCurrentDate);
+ });
+}
+
/**
* @namespace DateUtils
*/
const DateUtils = {
timestampToRelative,
timestampToDateTime,
+ startCurrentDateUpdater,
};
export default DateUtils;
diff --git a/src/libs/KeyboardAvoidingView/index.ios.js b/src/libs/KeyboardAvoidingView/index.ios.js
index fc08d4f52fa6..89ef7e033f02 100644
--- a/src/libs/KeyboardAvoidingView/index.ios.js
+++ b/src/libs/KeyboardAvoidingView/index.ios.js
@@ -27,4 +27,5 @@ function KeyboardAvoidingView({children}) {
KeyboardAvoidingView.propTypes = propTypes;
KeyboardAvoidingView.defaultProps = defaultProps;
+KeyboardAvoidingView.displayName = 'KeyboardAvoidingView';
export default KeyboardAvoidingView;
diff --git a/src/libs/KeyboardAvoidingView/index.js b/src/libs/KeyboardAvoidingView/index.js
index 894cbd36e6b5..985240a00777 100644
--- a/src/libs/KeyboardAvoidingView/index.js
+++ b/src/libs/KeyboardAvoidingView/index.js
@@ -2,10 +2,7 @@
* This is a KeyboardAvoidingView only enabled for ios && disabled for all other platforms
* @param {Node}
*/
-import React from 'react';
-import {KeyboardAvoidingView as KeyboardAvoidingViewComponent} from 'react-native';
import PropTypes from 'prop-types';
-import styles from '../../styles/styles';
const propTypes = {
children: PropTypes.node,
@@ -14,18 +11,9 @@ const defaultProps = {
children: null,
};
-function KeyboardAvoidingView({children}) {
- return (
-
- {children}
-
- );
-}
+const KeyboardAvoidingView = props => props.children;
KeyboardAvoidingView.propTypes = propTypes;
KeyboardAvoidingView.defaultProps = defaultProps;
+KeyboardAvoidingView.displayName = 'KeyboardAvoidingView';
export default KeyboardAvoidingView;
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js
index 08f0efbf6c54..912fbe06e388 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.js
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.js
@@ -51,6 +51,7 @@ import {
EnablePaymentsStackNavigator,
BusinessBankAccountModalStackNavigator,
AddPersonalBankAccountModalStackNavigator,
+ NewWorkspaceStackNavigator,
} from './ModalStackNavigators';
import SCREENS from '../../../SCREENS';
import Timers from '../../Timers';
@@ -73,11 +74,11 @@ Onyx.connect({
const RootStack = createCustomModalStackNavigator();
// We want to delay the re-rendering for components(e.g. ReportActionCompose)
-// that depends on modal visibility until Modal is completely closed or its transition has ended
-// When modal screen is focused and animation transition is ended, update modal visibility in Onyx
+// that depends on modal visibility until Modal is completely closed and its focused
+// When modal screen is focused, update modal visibility in Onyx
// https://reactnavigation.org/docs/navigation-events/
const modalScreenListeners = {
- transitionEnd: () => {
+ focus: () => {
setModalVisibility(true);
},
beforeRemove: () => {
@@ -121,6 +122,7 @@ class AuthScreens extends React.Component {
PersonalDetails.fetchPersonalDetails();
User.getUserDetails();
User.getBetas();
+ User.getPublicDomainInfo();
PersonalDetails.fetchCurrencyPreferences();
fetchAllReports(true, true);
fetchCountryCodeByRequestIP();
@@ -276,6 +278,11 @@ class AuthScreens extends React.Component {
component={AddPersonalBankAccountModalStackNavigator}
listeners={modalScreenListeners}
/>
+
{
// This is usually needed after login/create account and re-launches
return (
}
screenOptions={{
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
index f7de075cea8f..238629c640f6 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
@@ -22,6 +22,7 @@ import ReportParticipantsPage from '../../../pages/ReportParticipantsPage';
import EnablePaymentsPage from '../../../pages/EnablePayments';
import BusinessBankAccountNewPage from '../../../pages/BusinessBankAccount/NewPage';
import AddPersonalBankAccountPage from '../../../pages/AddPersonalBankAccountPage';
+import NewWorkspacePage from '../../../pages/workspace/NewWorkspacePage';
const defaultSubRouteOptions = {
cardStyle: styles.navigationScreenCardStyle,
@@ -153,6 +154,11 @@ const AddPersonalBankAccountModalStackNavigator = createModalStackNavigator([{
name: 'AddPersonalBankAccount_Root',
}]);
+const NewWorkspaceStackNavigator = createModalStackNavigator([{
+ Component: NewWorkspacePage,
+ name: 'NewWorkspace_Root',
+}]);
+
const BusinessBankAccountModalStackNavigator = createModalStackNavigator([
{
Component: BusinessBankAccountNewPage,
@@ -173,4 +179,5 @@ export {
EnablePaymentsStackNavigator,
BusinessBankAccountModalStackNavigator,
AddPersonalBankAccountModalStackNavigator,
+ NewWorkspaceStackNavigator,
};
diff --git a/src/libs/Navigation/AppNavigator/PublicScreens.js b/src/libs/Navigation/AppNavigator/PublicScreens.js
index 4241a7d42d33..62032beb27cc 100644
--- a/src/libs/Navigation/AppNavigator/PublicScreens.js
+++ b/src/libs/Navigation/AppNavigator/PublicScreens.js
@@ -2,6 +2,7 @@ import React from 'react';
import {createStackNavigator} from '@react-navigation/stack';
import SignInPage from '../../../pages/signin/SignInPage';
import SetPasswordPage from '../../../pages/SetPasswordPage';
+import PublicWorkspaceNewView from '../../../pages/workspace/PublicWorkspaceNewView';
import ValidateLoginPage from '../../../pages/ValidateLoginPage';
import SCREENS from '../../../SCREENS';
@@ -31,5 +32,9 @@ export default () => (
options={defaultScreenOptions}
component={SetPasswordPage}
/>
+
);
diff --git a/src/libs/Navigation/Navigation.js b/src/libs/Navigation/Navigation.js
index 7fe26fcf31d9..012b9dceccbc 100644
--- a/src/libs/Navigation/Navigation.js
+++ b/src/libs/Navigation/Navigation.js
@@ -1,7 +1,7 @@
import _ from 'underscore';
import React from 'react';
import {StackActions, DrawerActions} from '@react-navigation/native';
-
+import PropTypes from 'prop-types';
import Onyx from 'react-native-onyx';
import linkTo from './linkTo';
import ROUTES from '../../ROUTES';
@@ -98,8 +98,37 @@ function dismissModal(shouldOpenDrawer = false) {
openDrawer();
}
+/**
+ * Alternative to the `Navigation.dismissModal()` function that we can use inside
+ * the render function of other components to avoid breaking React rules about side-effects.
+ *
+ * Example:
+ * ```jsx
+ * if (!Permissions.canUseFreePlan(this.props.betas)) {
+ * return ;
+ * }
+ * ```
+ */
+class DismissModal extends React.Component {
+ componentDidMount() {
+ dismissModal(this.props.shouldOpenDrawer);
+ }
+
+ render() {
+ return null;
+ }
+}
+
+DismissModal.propTypes = {
+ shouldOpenDrawer: PropTypes.bool,
+};
+DismissModal.defaultProps = {
+ shouldOpenDrawer: false,
+};
+
export default {
navigate,
dismissModal,
goBack,
+ DismissModal,
};
diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js
index 720196767e8b..58ca48d1cae9 100644
--- a/src/libs/Navigation/linkingConfig.js
+++ b/src/libs/Navigation/linkingConfig.js
@@ -119,6 +119,11 @@ export default {
EnablePayments_Root: ROUTES.ENABLE_PAYMENTS,
},
},
+ NewWorkspace: {
+ screens: {
+ NewWorkspace_Root: ROUTES.WORKSPACE_NEW,
+ },
+ },
},
},
};
diff --git a/src/libs/Permissions.js b/src/libs/Permissions.js
index f0a981b350b3..384a5036f19a 100644
--- a/src/libs/Permissions.js
+++ b/src/libs/Permissions.js
@@ -43,9 +43,18 @@ function canUseFreePlan(betas) {
return _.contains(betas, CONST.BETAS.FREE_PLAN) || canUseAllBetas();
}
+/**
+ * @param {Array} betas
+ * @returns {Boolean}
+ */
+function canUseDefaultRooms(betas) {
+ return _.contains(betas, CONST.BETAS.DEFAULT_ROOMS) || canUseAllBetas();
+}
+
export default {
canUseChronos,
canUseIOU,
canUsePayWithExpensify,
canUseFreePlan,
+ canUseDefaultRooms,
};
diff --git a/src/libs/ReportActionComposeFocusManager.js b/src/libs/ReportActionComposeFocusManager.js
new file mode 100644
index 000000000000..e2aaa288ca89
--- /dev/null
+++ b/src/libs/ReportActionComposeFocusManager.js
@@ -0,0 +1,51 @@
+import _ from 'underscore';
+import React from 'react';
+
+const composerRef = React.createRef();
+let focusCallback = null;
+
+/**
+ * Register a callback to be called when focus is requested.
+ * Typical uses of this would be call the focus on the ReportActionComposer.
+ *
+ * @param {Function} callback callback to register
+ */
+function onComposerFocus(callback) {
+ focusCallback = callback;
+}
+
+/**
+ * Request focus on the ReportActionComposer
+ *
+ */
+function focus() {
+ if (_.isFunction(focusCallback)) {
+ focusCallback();
+ }
+}
+
+/**
+ * Clear the registered focus callback
+ *
+ */
+function clear() {
+ focusCallback = null;
+}
+
+/**
+ * Exposes the current focus state of the report action composer.
+ *
+ */
+function isFocused() {
+ if (composerRef.current) {
+ composerRef.current.isFocused();
+ }
+}
+
+export default {
+ composerRef,
+ onComposerFocus,
+ focus,
+ clear,
+ isFocused,
+};
diff --git a/src/libs/StatusBar/index.android.js b/src/libs/StatusBar/index.android.js
new file mode 100644
index 000000000000..0ea4480dc928
--- /dev/null
+++ b/src/libs/StatusBar/index.android.js
@@ -0,0 +1,4 @@
+import {StatusBar} from 'react-native';
+
+// Just export StatusBar – no changes.
+export default StatusBar;
diff --git a/src/libs/StatusBar/index.js b/src/libs/StatusBar/index.js
new file mode 100644
index 000000000000..4aea200cab00
--- /dev/null
+++ b/src/libs/StatusBar/index.js
@@ -0,0 +1,6 @@
+import {StatusBar} from 'react-native';
+
+// Overwrite setTranslucent to suppress a warning on iOS
+StatusBar.setTranslucent = () => {};
+
+export default StatusBar;
diff --git a/src/libs/actions/Session.js b/src/libs/actions/Session.js
index efbb82abe602..07d545059fad 100644
--- a/src/libs/actions/Session.js
+++ b/src/libs/actions/Session.js
@@ -23,11 +23,9 @@ Onyx.connect({
*/
function setSuccessfulSignInData(data) {
PushNotification.register(data.accountID);
- Onyx.multiSet({
- [ONYXKEYS.SESSION]: {
- shouldShowComposeInput: true,
- ..._.pick(data, 'authToken', 'accountID', 'email'),
- },
+ Onyx.merge(ONYXKEYS.SESSION, {
+ shouldShowComposeInput: true,
+ ..._.pick(data, 'authToken', 'accountID', 'email'),
});
}
diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js
index b7e7bdaf1b32..c43701d37c30 100644
--- a/src/libs/actions/User.js
+++ b/src/libs/actions/User.js
@@ -1,6 +1,8 @@
import _ from 'underscore';
import lodashGet from 'lodash/get';
import Onyx from 'react-native-onyx';
+import Str from 'expensify-common/lib/str';
+import {PUBLIC_DOMAINS as COMMON_PUBLIC_DOMAINS} from 'expensify-common/lib/CONST';
import ONYXKEYS from '../../ONYXKEYS';
import * as API from '../API';
import CONST from '../../CONST';
@@ -8,9 +10,13 @@ import Navigation from '../Navigation/Navigation';
import ROUTES from '../../ROUTES';
let sessionAuthToken = '';
+let sessionEmail = '';
Onyx.connect({
key: ONYXKEYS.SESSION,
- callback: val => sessionAuthToken = val ? val.authToken : '',
+ callback: (val) => {
+ sessionAuthToken = lodashGet(val, 'authToken', '');
+ sessionEmail = lodashGet(val, 'email', '');
+ },
});
let currentlyViewedReportID = '';
@@ -168,6 +174,38 @@ function validateLogin(accountID, validateCode) {
});
}
+/**
+ * Fetch the public domain info for the current user.
+ *
+ * This API is a bit weird in that it sometimes depends on information being cached in bedrock.
+ * If the info for the domain is not in bedrock, then it creates an asynchronous bedrock job to gather domain info.
+ * If that happens, this function will automatically retry itself in 10 minutes.
+ */
+function getPublicDomainInfo() {
+ // If this command fails, we'll retry again in 10 minutes,
+ // arbitrarily chosen giving Bedrock time to resolve the ClearbitCheckPublicEmail job for this email.
+ const RETRY_TIMEOUT = 600000;
+
+ // First check list of common public domains
+ if (_.contains(COMMON_PUBLIC_DOMAINS, sessionEmail)) {
+ Onyx.merge(ONYXKEYS.USER, {isFromPublicDomain: true});
+ return;
+ }
+
+ // If it is not a common public domain, check the API
+ API.User_IsFromPublicDomain({email: sessionEmail})
+ .then((response) => {
+ if (response.jsonCode === 200) {
+ const {isFromPublicDomain} = response;
+ Onyx.merge(ONYXKEYS.USER, {isFromPublicDomain});
+ } else {
+ // eslint-disable-next-line max-len
+ console.debug(`Command User_IsFromPublicDomain returned error code: ${response.jsonCode}. Most likely, this means that the domain ${Str.extractEmail(sessionEmail)} is not in the bedrock cache. Retrying in ${RETRY_TIMEOUT / 1000 / 60} minutes`);
+ setTimeout(getPublicDomainInfo, RETRY_TIMEOUT);
+ }
+ });
+}
+
export {
changePassword,
getBetas,
@@ -176,4 +214,5 @@ export {
setExpensifyNewsStatus,
setSecondaryLogin,
validateLogin,
+ getPublicDomainInfo,
};
diff --git a/src/pages/BusinessBankAccount/NewPage.js b/src/pages/BusinessBankAccount/NewPage.js
index 7e0bf0fd27c0..22fad1997dbb 100644
--- a/src/pages/BusinessBankAccount/NewPage.js
+++ b/src/pages/BusinessBankAccount/NewPage.js
@@ -71,8 +71,7 @@ class BusinessBankAccountNewPage extends React.Component {
render() {
if (!Permissions.canUseFreePlan(this.props.betas)) {
console.debug('Not showing new bank account page because user is not on free plan beta');
- Navigation.dismissModal();
- return null;
+ return ;
}
return (
diff --git a/src/pages/NewGroupPage.js b/src/pages/NewGroupPage.js
index f7add0b42ba0..e0511756c8a9 100755
--- a/src/pages/NewGroupPage.js
+++ b/src/pages/NewGroupPage.js
@@ -9,7 +9,6 @@ import ONYXKEYS from '../ONYXKEYS';
import styles from '../styles/styles';
import {fetchOrCreateChatReport} from '../libs/actions/Report';
import CONST from '../CONST';
-import KeyboardSpacer from '../components/KeyboardSpacer';
import withWindowDimensions, {windowDimensionsPropTypes} from '../components/withWindowDimensions';
import HeaderWithCloseButton from '../components/HeaderWithCloseButton';
import ScreenWrapper from '../components/ScreenWrapper';
@@ -18,6 +17,8 @@ import FullScreenLoadingIndicator from '../components/FullscreenLoadingIndicator
import withLocalize, {withLocalizePropTypes} from '../components/withLocalize';
import compose from '../libs/compose';
import Button from '../components/Button';
+import KeyboardAvoidingView from '../libs/KeyboardAvoidingView';
+import FixedFooter from '../components/FixedFooter';
const personalDetailsPropTypes = PropTypes.shape({
/** The login of the person (either email or phone number) */
@@ -194,7 +195,7 @@ class NewGroupPage extends Component {
return (
{({didScreenTransitionEnd}) => (
- <>
+
Navigation.dismissModal(true)}
@@ -203,53 +204,54 @@ class NewGroupPage extends Component {
{didScreenTransitionEnd && (
<>
- {
- const {
- recentReports,
- personalDetails,
- userToInvite,
- } = getNewGroupOptions(
- this.props.reports,
- this.props.personalDetails,
- searchValue,
- [],
- false,
- this.props.betas,
- );
- this.setState({
- searchValue,
- userToInvite,
- recentReports,
- personalDetails,
- });
- }}
- headerMessage={headerMessage}
- disableArrowKeysActions
- hideAdditionalOptionStates
- forceTextUnreadStyle
- shouldFocusOnSelectRow
- />
- {this.state.selectedOptions?.length > 0 && (
-
-
+ {this.state.selectedOptions?.length > 0 && (
+
+
+
)}
>
)}
-
- >
+
)}
);
diff --git a/src/pages/SetPasswordPage.js b/src/pages/SetPasswordPage.js
index 29ebc213033e..99f79f8f9053 100755
--- a/src/pages/SetPasswordPage.js
+++ b/src/pages/SetPasswordPage.js
@@ -2,7 +2,6 @@ import React, {Component} from 'react';
import {
SafeAreaView,
Text,
- TextInput,
View,
} from 'react-native';
import PropTypes from 'prop-types';
@@ -14,11 +13,10 @@ import styles from '../styles/styles';
import {setPassword} from '../libs/actions/Session';
import ONYXKEYS from '../ONYXKEYS';
import Button from '../components/Button';
-import themeColors from '../styles/themes/default';
import SignInPageLayout from './signin/SignInPageLayout';
-import canFocusInputOnScreenFocus from '../libs/canFocusInputOnScreenFocus';
import withLocalize, {withLocalizePropTypes} from '../components/withLocalize';
import compose from '../libs/compose';
+import NewPasswordForm from './settings/NewPasswordForm';
const propTypes = {
/* Onyx Props */
@@ -63,7 +61,7 @@ class SetPasswordPage extends Component {
this.state = {
password: '',
- formError: null,
+ isFormValid: false,
};
}
@@ -71,16 +69,9 @@ class SetPasswordPage extends Component {
* Validate the form and then submit it
*/
validateAndSubmitForm() {
- if (!this.state.password.trim()) {
- this.setState({
- formError: this.props.translate('setPasswordPage.passwordCannotBeBlank'),
- });
+ if (!this.state.isFormValid) {
return;
}
-
- this.setState({
- formError: null,
- });
setPassword(
this.state.password,
lodashGet(this.props.route, 'params.validateCode', ''),
@@ -93,20 +84,11 @@ class SetPasswordPage extends Component {
-
- {this.props.translate('setPasswordPage.enterPassword')}
-
- this.setState({password: text})}
+ this.setState({password})}
+ updateIsFormValid={isValid => this.setState({isFormValid: isValid})}
onSubmitEditing={this.validateAndSubmitForm}
- autoCapitalize="none"
- placeholderTextColor={themeColors.placeholderText}
- autoFocus={canFocusInputOnScreenFocus()}
/>
@@ -119,12 +101,6 @@ class SetPasswordPage extends Component {
/>
- {this.state.formError && (
-
- {this.state.formError}
-
- )}
-
{!_.isEmpty(this.props.account.error) && (
{this.props.account.error}
diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js
index d5c205af9c47..d8d183de2511 100644
--- a/src/pages/home/HeaderView.js
+++ b/src/pages/home/HeaderView.js
@@ -65,7 +65,7 @@ const HeaderView = (props) => {
return {
displayName: (isMultipleParticipant ? firstName : displayNameTrimmed) || Str.removeSMSDomain(login),
- tooltip: login,
+ tooltip: Str.removeSMSDomain(login),
};
},
);
diff --git a/src/pages/home/report/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose.js
index af29d8387657..473635e141e3 100755
--- a/src/pages/home/report/ReportActionCompose.js
+++ b/src/pages/home/report/ReportActionCompose.js
@@ -51,6 +51,7 @@ import Navigation from '../../../libs/Navigation/Navigation';
import ROUTES from '../../../ROUTES';
import ReportActionPropTypes from './ReportActionPropTypes';
import {canEditReportAction} from '../../../libs/reportUtils';
+import ReportActionComposeFocusManager from '../../../libs/ReportActionComposeFocusManager';
const propTypes = {
/** Beta features list */
@@ -129,6 +130,7 @@ class ReportActionCompose extends React.Component {
this.onSelectionChange = this.onSelectionChange.bind(this);
this.emojiPopoverAnchor = null;
this.emojiSearchInput = null;
+ this.setTextInputRef = this.setTextInputRef.bind(this);
this.state = {
isFocused: this.shouldFocusInputOnScreenFocus,
@@ -150,6 +152,7 @@ class ReportActionCompose extends React.Component {
}
componentDidMount() {
+ ReportActionComposeFocusManager.onComposerFocus(this.focus);
Dimensions.addEventListener('change', this.measureEmojiPopoverAnchorPosition);
}
@@ -165,6 +168,7 @@ class ReportActionCompose extends React.Component {
}
componentWillUnmount() {
+ ReportActionComposeFocusManager.clear();
Dimensions.removeEventListener('change', this.measureEmojiPopoverAnchorPosition);
}
@@ -199,11 +203,22 @@ class ReportActionCompose extends React.Component {
this.setState({isMenuVisible});
}
+ /**
+ * Set the TextInput Ref
+ *
+ * @param {Element} el
+ * @memberof ReportActionCompose
+ */
+ setTextInputRef(el) {
+ ReportActionComposeFocusManager.composerRef.current = el;
+ this.textInput = el;
+ }
+
/**
* Focus the composer text input
*/
focus() {
- if (this.textInput) {
+ if (this.shouldFocusInputOnScreenFocus && this.props.isFocused && this.textInput) {
// There could be other animations running while we trigger manual focus.
// This prevents focus from making those animations janky.
InteractionManager.runAfterInteractions(() => {
@@ -446,7 +461,7 @@ class ReportActionCompose extends React.Component {
this.textInput = el}
+ ref={this.setTextInputRef}
textAlignVertical="top"
placeholder={this.props.translate('reportActionCompose.writeSomething')}
placeholderTextColor={themeColors.placeholderText}
diff --git a/src/pages/home/report/ReportActionContextMenu.js b/src/pages/home/report/ReportActionContextMenu.js
index bd4ee0bc481d..c38b8d7db847 100755
--- a/src/pages/home/report/ReportActionContextMenu.js
+++ b/src/pages/home/report/ReportActionContextMenu.js
@@ -18,6 +18,7 @@ import compose from '../../../libs/compose';
import {isReportMessageAttachment, canEditReportAction} from '../../../libs/reportUtils';
import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
import ConfirmModal from '../../../components/ConfirmModal';
+import ReportActionComposeFocusManager from '../../../libs/ReportActionComposeFocusManager';
const propTypes = {
/** The ID of the report this report action is attached to. */
@@ -87,7 +88,7 @@ class ReportActionContextMenu extends React.Component {
} else {
Clipboard.setString(html);
}
- this.hidePopover(true);
+ this.hidePopover(true, ReportActionComposeFocusManager.focus);
},
},
@@ -106,7 +107,7 @@ class ReportActionContextMenu extends React.Component {
onPress: () => {
updateLastReadActionID(this.props.reportID, this.props.reportAction.sequenceNumber);
setNewMarkerPosition(this.props.reportID, this.props.reportAction.sequenceNumber);
- this.hidePopover(true);
+ this.hidePopover(true, ReportActionComposeFocusManager.focus);
},
},
@@ -115,21 +116,26 @@ class ReportActionContextMenu extends React.Component {
icon: Pencil,
shouldShow: () => canEditReportAction(this.props.reportAction),
onPress: () => {
- this.hidePopover();
- saveReportActionDraft(
+ const editAction = () => saveReportActionDraft(
this.props.reportID,
this.props.reportAction.reportActionID,
_.isEmpty(this.props.draftMessage) ? this.getActionText() : '',
);
+
+ if (this.props.isMini) {
+ // No popover to hide, call editAction immediately
+ editAction();
+ } else {
+ // Hide popover, then call editAction
+ this.hidePopover(false, editAction);
+ }
},
},
{
text: this.props.translate('reportActionContextMenu.deleteComment'),
icon: Trashcan,
shouldShow: () => canEditReportAction(this.props.reportAction),
- onPress: () => {
- this.setState({isDeleteCommentConfirmModalVisible: true});
- },
+ onPress: () => this.setState({isDeleteCommentConfirmModalVisible: true}),
},
];
@@ -165,14 +171,15 @@ class ReportActionContextMenu extends React.Component {
* Hides the popover menu with an optional delay
*
* @param {Boolean} shouldDelay whether the menu should close after a delay
+ * @param {Function} [onHideCallback=() => {}] Callback to be called after Popover Menu is hidden
* @memberof ReportActionContextMenu
*/
- hidePopover(shouldDelay) {
+ hidePopover(shouldDelay, onHideCallback = () => {}) {
if (!shouldDelay) {
- this.props.hidePopover();
+ this.props.hidePopover(onHideCallback);
return;
}
- setTimeout(this.props.hidePopover, 800);
+ setTimeout(() => this.props.hidePopover(onHideCallback), 800);
}
render() {
diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js
index f71726ec1cae..9b8b7d6d0947 100644
--- a/src/pages/home/report/ReportActionItem.js
+++ b/src/pages/home/report/ReportActionItem.js
@@ -61,6 +61,7 @@ class ReportActionItem extends Component {
constructor(props) {
super(props);
+ this.onPopoverHide = () => {};
this.state = {
isPopoverVisible: false,
cursorPosition: {
@@ -173,8 +174,12 @@ class ReportActionItem extends Component {
/**
* Hide the ReportActionContextMenu modal popover.
+ * @param {Function} onHideCallback Callback to be called after popover is completely hidden
*/
- hidePopover() {
+ hidePopover(onHideCallback) {
+ if (_.isFunction(onHideCallback)) {
+ this.onPopoverHide = onHideCallback;
+ }
this.setState({isPopoverVisible: false});
}
@@ -268,10 +273,12 @@ class ReportActionItem extends Component {
(
ReportActionItemDate.propTypes = propTypes;
ReportActionItemDate.displayName = 'ReportActionItemDate';
-export default withLocalize(memo(ReportActionItemDate));
+export default compose(
+ withLocalize,
+ withOnyx({
+ currentDate: {
+ key: ONYXKEYS.CURRENT_DATE,
+ },
+ }),
+ memo,
+)(ReportActionItemDate);
diff --git a/src/pages/home/report/ReportActionItemMessageEdit.js b/src/pages/home/report/ReportActionItemMessageEdit.js
index 5e9bb1ec625d..2a7d49b5e2c8 100644
--- a/src/pages/home/report/ReportActionItemMessageEdit.js
+++ b/src/pages/home/report/ReportActionItemMessageEdit.js
@@ -10,6 +10,7 @@ import {scrollToIndex} from '../../../libs/ReportScrollManager';
import toggleReportActionComposeView from '../../../libs/toggleReportActionComposeView';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions';
import Button from '../../../components/Button';
+import ReportActionComposeFocusManager from '../../../libs/ReportActionComposeFocusManager';
const propTypes = {
/** All the data of the action */
@@ -74,6 +75,7 @@ class ReportActionItemMessageEdit extends React.Component {
deleteDraft() {
saveReportActionDraft(this.props.reportID, this.props.action.reportActionID, '');
toggleReportActionComposeView(true, this.props.isSmallScreenWidth);
+ ReportActionComposeFocusManager.focus();
}
/**
diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js
index 39d215aed6e2..ff0641e50046 100755
--- a/src/pages/home/report/ReportActionsView.js
+++ b/src/pages/home/report/ReportActionsView.js
@@ -32,6 +32,7 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '../../../componen
import withDrawerState, {withDrawerPropTypes} from '../../../components/withDrawerState';
import {flatListRef, scrollToBottom} from '../../../libs/ReportScrollManager';
import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
+import ReportActionComposeFocusManager from '../../../libs/ReportActionComposeFocusManager';
const propTypes = {
/** The ID of the report actions will be created for */
@@ -107,7 +108,11 @@ class ReportActionsView extends React.Component {
componentDidMount() {
AppState.addEventListener('change', this.onVisibilityChange);
subscribeToReportTypingEvents(this.props.reportID);
- this.keyboardEvent = Keyboard.addListener('keyboardDidShow', this.scrollToListBottom);
+ this.keyboardEvent = Keyboard.addListener('keyboardDidShow', () => {
+ if (ReportActionComposeFocusManager.isFocused()) {
+ this.scrollToListBottom();
+ }
+ });
updateLastReadActionID(this.props.reportID);
// Since we want the New marker to remain in place even if newer messages come in, we set it once on mount.
diff --git a/src/pages/home/sidebar/OptionRow.js b/src/pages/home/sidebar/OptionRow.js
index 9fed30b58560..47fee412d3f3 100644
--- a/src/pages/home/sidebar/OptionRow.js
+++ b/src/pages/home/sidebar/OptionRow.js
@@ -135,7 +135,7 @@ const OptionRow = ({
return {
displayName: (isMultipleParticipant ? firstName : displayNameTrimmed) || Str.removeSMSDomain(login),
- tooltip: login,
+ tooltip: Str.removeSMSDomain(login),
};
},
);
diff --git a/src/pages/iou/IOUCurrencySelection.js b/src/pages/iou/IOUCurrencySelection.js
index 3218b62f2f2b..861789de5e30 100644
--- a/src/pages/iou/IOUCurrencySelection.js
+++ b/src/pages/iou/IOUCurrencySelection.js
@@ -1,5 +1,5 @@
import React, {Component} from 'react';
-import {Pressable, SectionList, View} from 'react-native';
+import {SectionList, View} from 'react-native';
import PropTypes from 'prop-types';
import Onyx, {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
@@ -17,6 +17,9 @@ import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
import compose from '../../libs/compose';
import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
import CONST from '../../CONST';
+import KeyboardAvoidingView from '../../libs/KeyboardAvoidingView';
+import Button from '../../components/Button';
+import FixedFooter from '../../components/FixedFooter';
/**
* IOU Currency selection for selecting currency
@@ -157,69 +160,65 @@ class IOUCurrencySelection extends Component {
render() {
return (
- Navigation.goBack()}
- />
-
-
-
- this.textInput = el}
- style={[styles.textInput]}
- value={this.state.searchValue}
- onChangeText={this.changeSearchValue}
- placeholder="Search"
- placeholderTextColor={themeColors.placeholderText}
- />
-
+
+ Navigation.goBack()}
+ />
+
- option.currencyCode}
- stickySectionHeadersEnabled={false}
- renderItem={({item, key}) => (
- this.toggleOption(item.currencyCode)}
- isSelected={item.currencyCode === this.state.selectedCurrency.currencyCode}
- showSelectedState
- hideAdditionalOptionStates
- />
- )}
- renderSectionHeader={({section: {title}}) => (
-
-
- {title}
-
-
- )}
- />
+
+ this.textInput = el}
+ style={[styles.textInput]}
+ value={this.state.searchValue}
+ onChangeText={this.changeSearchValue}
+ placeholder={this.props.translate('common.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}
+
+
+ )}
+ />
+
-
-
+
-
-
+ />
+
+
);
}
diff --git a/src/pages/iou/IOUModal.js b/src/pages/iou/IOUModal.js
index 35d73f7dad16..36f130e6f76e 100755
--- a/src/pages/iou/IOUModal.js
+++ b/src/pages/iou/IOUModal.js
@@ -20,6 +20,7 @@ import {getPersonalDetailsForLogins} from '../../libs/OptionsListUtils';
import FullScreenLoadingIndicator from '../../components/FullscreenLoadingIndicator';
import ScreenWrapper from '../../components/ScreenWrapper';
import CONST from '../../CONST';
+import KeyboardAvoidingView from '../../libs/KeyboardAvoidingView';
/**
* IOU modal for requesting money and splitting bills.
@@ -268,7 +269,7 @@ class IOUModal extends Component {
return (
{({didScreenTransitionEnd}) => (
- <>
+
)}
- >
+
)}
);
diff --git a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js
index 2aa79d7b9e6e..42a100d00e5c 100755
--- a/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js
+++ b/src/pages/iou/steps/IOUParticipantsPage/IOUParticipantsSplit.js
@@ -11,6 +11,7 @@ import CONST from '../../../../CONST';
import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize';
import compose from '../../../../libs/compose';
import Button from '../../../../components/Button';
+import FixedFooter from '../../../../components/FixedFooter';
const personalDetailsPropTypes = PropTypes.shape({
// The login of the person (either email or phone number)
@@ -197,51 +198,53 @@ class IOUParticipantsSplit extends Component {
const maxParticipantsReached = this.props.participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS;
const sections = this.getSections(maxParticipantsReached);
return (
-
-
- {this.props.translate('common.to')}
-
- {
- const {
- recentReports,
- personalDetails,
- userToInvite,
- } = getNewGroupOptions(
- this.props.reports,
- this.props.personalDetails,
- searchValue,
- [],
- true,
- this.props.betas,
- );
- this.setState({
- searchValue,
- userToInvite,
- recentReports,
- personalDetails,
- });
- }}
- disableArrowKeysActions
- hideAdditionalOptionStates
- forceTextUnreadStyle
- />
+ <>
+
+
+ {this.props.translate('common.to')}
+
+ {
+ const {
+ recentReports,
+ personalDetails,
+ userToInvite,
+ } = getNewGroupOptions(
+ this.props.reports,
+ this.props.personalDetails,
+ searchValue,
+ [],
+ true,
+ this.props.betas,
+ );
+ this.setState({
+ searchValue,
+ userToInvite,
+ recentReports,
+ personalDetails,
+ });
+ }}
+ disableArrowKeysActions
+ hideAdditionalOptionStates
+ forceTextUnreadStyle
+ />
+
{this.props.participants?.length > 0 && (
-
+
-
+
)}
-
+ >
);
}
}
diff --git a/src/pages/settings/AboutPage.js b/src/pages/settings/AboutPage.js
index c273ae70ca27..f337c2bae552 100644
--- a/src/pages/settings/AboutPage.js
+++ b/src/pages/settings/AboutPage.js
@@ -108,7 +108,7 @@ const AboutPage = ({translate, session}) => {
{menuItems.map(item => (
)}
-
-
+
+
-
-
+
+
);
}
diff --git a/src/pages/settings/PaymentsPage.js b/src/pages/settings/PaymentsPage.js
index 1f6427f667e8..a038b74a8b04 100755
--- a/src/pages/settings/PaymentsPage.js
+++ b/src/pages/settings/PaymentsPage.js
@@ -15,6 +15,8 @@ import styles from '../../styles/styles';
import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
import compose from '../../libs/compose';
import Button from '../../components/Button';
+import KeyboardAvoidingView from '../../libs/KeyboardAvoidingView';
+import FixedFooter from '../../components/FixedFooter';
import Growl from '../../libs/Growl';
const propTypes = {
@@ -61,38 +63,42 @@ class PaymentsPage extends React.Component {
render() {
return (
- Navigation.navigate(ROUTES.SETTINGS)}
- onCloseButtonPress={() => Navigation.dismissModal(true)}
- />
-
-
-
- {this.props.translate('paymentsPage.enterYourUsernameToGetPaidViaPayPal')}
-
-
- {this.props.translate('paymentsPage.payPalMe')}
-
- this.setState({payPalMeUsername: text})}
- editable={!this.props.payPalMeUsername}
- />
-
-
+
+
+
+ {this.props.translate('paymentsPage.enterYourUsernameToGetPaidViaPayPal')}
+
+
+ {this.props.translate('paymentsPage.payPalMe')}
+
+ this.setState({payPalMeUsername: text})}
+ editable={!this.props.payPalMeUsername}
+ />
+
+
+
+
+
+
);
}
diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js
index 7fade8151c0c..2cce3b772dbb 100755
--- a/src/pages/settings/Profile/ProfilePage.js
+++ b/src/pages/settings/Profile/ProfilePage.js
@@ -30,6 +30,8 @@ import Picker from '../../../components/Picker';
import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
import compose from '../../../libs/compose';
import Button from '../../../components/Button';
+import KeyboardAvoidingView from '../../../libs/KeyboardAvoidingView';
+import FixedFooter from '../../../components/FixedFooter';
import Growl from '../../../libs/Growl';
const propTypes = {
@@ -260,139 +262,141 @@ class ProfilePage extends Component {
return (
- Navigation.navigate(ROUTES.SETTINGS)}
- onCloseButtonPress={() => Navigation.dismissModal(true)}
- />
-
-
+ Navigation.navigate(ROUTES.SETTINGS)}
+ onCloseButtonPress={() => Navigation.dismissModal(true)}
/>
-
- {({openPicker}) => (
- <>
-
+
+ {this.props.translate('profilePage.tellUsAboutYourself')}
+
+
+
+
+ {this.props.translate('profilePage.firstName')}
+
+ this.setState({firstName})}
+ placeholder={this.props.translate('profilePage.john')}
+ placeholderTextColor={themeColors.placeholderText}
/>
- this.setState({isEditPhotoMenuVisible: false})}
- onItemSelected={() => this.setState({isEditPhotoMenuVisible: false})}
- menuItems={this.createMenuItems(openPicker)}
- anchorPosition={styles.createMenuPositionProfile}
- animationIn="fadeInRight"
- animationOut="fadeOutRight"
+
+
+
+ {this.props.translate('profilePage.lastName')}
+
+ this.setState({lastName})}
+ placeholder={this.props.translate('profilePage.doe')}
+ placeholderTextColor={themeColors.placeholderText}
/>
- >
- )}
-
-
- {this.props.translate('profilePage.tellUsAboutYourself')}
-
-
-
+
+
+
- {this.props.translate('profilePage.firstName')}
+ {this.props.translate('profilePage.preferredPronouns')}
+
+ this.setState({pronouns, selfSelectedPronouns: ''})}
+ items={this.pronounDropdownValues}
+ placeholder={{
+ value: '',
+ label: this.props.translate('profilePage.selectYourPronouns'),
+ }}
+ value={this.state.pronouns}
+ icon={() => }
+ />
+
+ {this.state.pronouns === this.props.translate('pronouns.selfSelect') && (
this.setState({firstName})}
- placeholder={this.props.translate('profilePage.john')}
+ value={this.state.selfSelectedPronouns}
+ onChangeText={selfSelectedPronouns => this.setState({selfSelectedPronouns})}
+ placeholder={this.props.translate('profilePage.selfSelectYourPronoun')}
placeholderTextColor={themeColors.placeholderText}
/>
+ )}
-
+
+
+
- {this.props.translate('profilePage.lastName')}
+ {this.props.translate('profilePage.timezone')}
- this.setState({lastName})}
- placeholder={this.props.translate('profilePage.doe')}
- placeholderTextColor={themeColors.placeholderText}
- />
-
-
-
-
- {this.props.translate('profilePage.preferredPronouns')}
-
-
this.setState({pronouns, selfSelectedPronouns: ''})}
- items={this.pronounDropdownValues}
- placeholder={{
- value: '',
- label: this.props.translate('profilePage.selectYourPronouns'),
- }}
- value={this.state.pronouns}
+ onChange={selectedTimezone => this.setState({selectedTimezone})}
+ items={timezones}
+ useDisabledStyles={this.state.isAutomaticTimezone}
+ value={this.state.selectedTimezone}
icon={() => }
+ disabled={this.state.isAutomaticTimezone}
/>
- {this.state.pronouns === this.props.translate('pronouns.selfSelect') && (
- this.setState({selfSelectedPronouns})}
- placeholder={this.props.translate('profilePage.selfSelectYourPronoun')}
- placeholderTextColor={themeColors.placeholderText}
+
- )}
-
-
-
-
-
- {this.props.translate('profilePage.timezone')}
-
- this.setState({selectedTimezone})}
- items={timezones}
- useDisabledStyles={this.state.isAutomaticTimezone}
- value={this.state.selectedTimezone}
- icon={() => }
- disabled={this.state.isAutomaticTimezone}
+
+
+
-
-
-
-
-
-
+
+
);
}
diff --git a/src/pages/workspace/NewWorkspacePage.js b/src/pages/workspace/NewWorkspacePage.js
new file mode 100644
index 000000000000..3a70fa29a130
--- /dev/null
+++ b/src/pages/workspace/NewWorkspacePage.js
@@ -0,0 +1,162 @@
+import React from 'react';
+import {View, Text} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import PropTypes from 'prop-types';
+import ONYXKEYS from '../../ONYXKEYS';
+import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
+import ScreenWrapper from '../../components/ScreenWrapper';
+import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
+import Navigation from '../../libs/Navigation/Navigation';
+import Permissions from '../../libs/Permissions';
+import styles from '../../styles/styles';
+import WorkspaceDefaultAvatar from '../../../assets/images/workspace-default-avatar.svg';
+import TextInputWithLabel from '../../components/TextInputWithLabel';
+import Button from '../../components/Button';
+import AttachmentPicker from '../../components/AttachmentPicker';
+import Icon from '../../components/Icon';
+import {DownArrow, Upload} from '../../components/Icon/Expensicons';
+import CreateMenu from '../../components/CreateMenu';
+import Switch from '../../components/Switch';
+import compose from '../../libs/compose';
+
+
+const propTypes = {
+ /** List of betas */
+ betas: PropTypes.arrayOf(PropTypes.string).isRequired,
+
+ ...withLocalizePropTypes,
+};
+
+class NewWorkspacePage extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ isEditPhotoMenuVisible: false,
+ name: '',
+ requestCall: false,
+ };
+
+ this.createMenuItems = this.createMenuItems.bind(this);
+ }
+
+ /**
+ * Create menu items list for avatar menu
+ *
+ * @param {Function} openPicker
+ * @returns {Array}
+ */
+ createMenuItems(openPicker) {
+ const menuItems = [
+ {
+ icon: Upload,
+ text: this.props.translate('workspace.new.uploadPhoto'),
+ onSelected: () => {
+ openPicker({
+ onPicked: () => {
+ // TODO: connect with setWorkspaceAvatar function
+ },
+ });
+ },
+ },
+ ];
+
+ // TODO: Add option to remove avatar if the user doesn't like the one they chose.
+
+ return menuItems;
+ }
+
+ render() {
+ if (!Permissions.canUseFreePlan(this.props.betas)) {
+ console.debug('Not showing new workspace page because user is not on free plan beta');
+ return ;
+ }
+
+ return (
+
+
+
+
+ {/* TODO: replace this with the Avatar component once we connect it with the backend */}
+
+
+
+ {({openPicker}) => (
+ <>
+ this.setState({isEditPhotoMenuVisible: true})}
+ ContentComponent={() => (
+
+
+
+
+ {this.props.translate('workspace.new.editPhoto')}
+
+
+
+ )}
+ />
+ this.setState({isEditPhotoMenuVisible: false})}
+ onItemSelected={() => this.setState({isEditPhotoMenuVisible: false})}
+ menuItems={this.createMenuItems(openPicker)}
+ anchorPosition={styles.createMenuPositionProfile}
+ animationIn="fadeInRight"
+ animationOut="fadeOutRight"
+ />
+ >
+ )}
+
+
+
+ this.setState({name})}
+ />
+ {this.props.translate('workspace.new.helpText')}
+
+
+
+ {this.props.translate('workspace.new.requestCall')}
+
+
+
+ this.setState({requestCall})}
+ />
+
+
+
+
+
+
+
+ );
+ }
+}
+
+NewWorkspacePage.propTypes = propTypes;
+
+export default compose(
+ withOnyx({
+ betas: {
+ key: ONYXKEYS.BETAS,
+ },
+ }),
+ withLocalize,
+)(NewWorkspacePage);
diff --git a/src/pages/workspace/PublicWorkspaceNewView.js b/src/pages/workspace/PublicWorkspaceNewView.js
new file mode 100644
index 000000000000..fa0ac88b4420
--- /dev/null
+++ b/src/pages/workspace/PublicWorkspaceNewView.js
@@ -0,0 +1,28 @@
+import React from 'react';
+import Onyx from 'react-native-onyx';
+import PropTypes from 'prop-types';
+import ONYXKEYS from '../../ONYXKEYS';
+import SCREENS from '../../SCREENS';
+
+const propTypes = {
+ /** react-navigation navigation object available to screen components */
+ navigation: PropTypes.shape({
+ /** Method used to navigate to a new page and not keep the current route in the history */
+ replace: PropTypes.func.isRequired,
+ }).isRequired,
+};
+
+class PublicWorkspaceNewView extends React.PureComponent {
+ componentDidMount() {
+ Onyx.merge(ONYXKEYS.SESSION, {redirectToWorkspaceNewAfterSignIn: true});
+ this.props.navigation.replace(SCREENS.HOME);
+ }
+
+ render() {
+ return null;
+ }
+}
+
+PublicWorkspaceNewView.propTypes = propTypes;
+
+export default PublicWorkspaceNewView;
diff --git a/src/setup/index.desktop.js b/src/setup/index.desktop.js
index f1c9364e7ab3..67c07723fd64 100644
--- a/src/setup/index.desktop.js
+++ b/src/setup/index.desktop.js
@@ -2,6 +2,7 @@ import {AppRegistry} from 'react-native';
import {ipcRenderer} from 'electron';
import Config from '../CONFIG';
import LocalNotification from '../libs/Notification/LocalNotification';
+import DateUtils from '../libs/DateUtils';
export default function () {
@@ -12,4 +13,7 @@ export default function () {
ipcRenderer.on('update-downloaded', () => {
LocalNotification.showUpdateAvailableNotification();
});
+
+ // Start current date updater
+ DateUtils.startCurrentDateUpdater();
}
diff --git a/src/setup/index.website.js b/src/setup/index.website.js
index 18e4d7df0505..a5c603188451 100644
--- a/src/setup/index.website.js
+++ b/src/setup/index.website.js
@@ -2,6 +2,7 @@ import {AppRegistry} from 'react-native';
import checkForUpdates from '../libs/checkForUpdates';
import Config from '../CONFIG';
import HttpUtils from '../libs/HttpUtils';
+import DateUtils from '../libs/DateUtils';
import {version as currentVersion} from '../../package.json';
import Visibility from '../libs/Visibility';
@@ -56,4 +57,7 @@ export default function () {
if (Config.IS_IN_PRODUCTION) {
checkForUpdates(webUpdater());
}
+
+ // Start current date updater
+ DateUtils.startCurrentDateUpdater();
}
diff --git a/src/styles/getModalStyles.js b/src/styles/getModalStyles.js
index 8921167c1cb1..f3a03dd92ae7 100644
--- a/src/styles/getModalStyles.js
+++ b/src/styles/getModalStyles.js
@@ -85,6 +85,37 @@ export default (type, windowDimensions, popoverAnchorPosition = {}) => {
animationOut = isSmallScreenWidth ? 'slideOutRight' : 'fadeOut';
shouldAddTopSafeAreaPadding = true;
break;
+ case CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE:
+ // A centered modal that cannot be dismissed with a swipe.
+ modalStyle = {
+ ...modalStyle,
+ ...{
+ alignItems: 'center',
+ },
+ };
+ modalContainerStyle = {
+ // Shadow Styles
+ shadowColor: colors.black,
+ shadowOffset: {
+ width: 0,
+ height: 0,
+ },
+ shadowOpacity: 0.1,
+ shadowRadius: 5,
+
+ flex: 1,
+ marginTop: isSmallScreenWidth ? 0 : 20,
+ marginBottom: isSmallScreenWidth ? 0 : 20,
+ borderRadius: isSmallScreenWidth ? 0 : 12,
+ borderWidth: isSmallScreenWidth ? 1 : 0,
+ overflow: 'hidden',
+ width: isSmallScreenWidth ? '100%' : windowWidth - 40,
+ };
+ swipeDirection = undefined;
+ animationIn = isSmallScreenWidth ? 'slideInRight' : 'fadeIn';
+ animationOut = isSmallScreenWidth ? 'slideOutRight' : 'fadeOut';
+ shouldAddTopSafeAreaPadding = true;
+ break;
case CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED:
modalStyle = {
...modalStyle,
diff --git a/tests/unit/loginTest.js b/tests/unit/loginTest.js
index c8bcc6c6f00f..615790e4b38a 100644
--- a/tests/unit/loginTest.js
+++ b/tests/unit/loginTest.js
@@ -9,6 +9,13 @@ import React from 'react';
import renderer from 'react-test-renderer';
import App from '../../src/App';
+/* uses and we need to mock the imported crashlytics module
+* due to an error that happens otherwise https://github.com/invertase/react-native-firebase/issues/2475 */
+jest.mock('@react-native-firebase/crashlytics', () => () => ({
+ log: jest.fn(),
+ recordError: jest.fn(),
+}));
+
describe('AppComponent', () => {
it('renders correctly', () => {
renderer.create();