diff --git a/assets/images/expensify-app-icon.svg b/assets/images/expensify-app-icon.svg
new file mode 100644
index 000000000000..a0adfe7dd952
--- /dev/null
+++ b/assets/images/expensify-app-icon.svg
@@ -0,0 +1,18 @@
+
+
+
diff --git a/src/Expensify.js b/src/Expensify.js
index a1c398d0bb51..ede42c2873dd 100644
--- a/src/Expensify.js
+++ b/src/Expensify.js
@@ -31,6 +31,7 @@ import AppleAuthWrapper from './components/SignInButtons/AppleAuthWrapper';
import EmojiPicker from './components/EmojiPicker/EmojiPicker';
import * as EmojiPickerAction from './libs/actions/EmojiPickerAction';
import * as DemoActions from './libs/actions/DemoActions';
+import DownloadAppModal from './components/DownloadAppModal';
import DeeplinkWrapper from './components/DeeplinkWrapper';
// This lib needs to be imported, but it has nothing to export since all it contains is an Onyx connection
@@ -198,6 +199,7 @@ function Expensify(props) {
+
{/* We include the modal for showing a new update at the top level so the option is always present. */}
{props.updateAvailable ? : null}
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 074a5e99e6b1..d4d2ab1f90a6 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -87,6 +87,9 @@ const ONYXKEYS = {
SESSION: 'session',
BETAS: 'betas',
+ /** Denotes if the Download App Banner has been dismissed */
+ SHOW_DOWNLOAD_APP_BANNER: 'showDownloadAppBanner',
+
/** NVP keys
* Contains the user's payPalMe data */
PAYPAL: 'paypal',
@@ -286,6 +289,7 @@ type OnyxValues = {
[ONYXKEYS.ACTIVE_CLIENTS]: string[];
[ONYXKEYS.DEVICE_ID]: string;
[ONYXKEYS.IS_SIDEBAR_LOADED]: boolean;
+ [ONYXKEYS.SHOW_DOWNLOAD_APP_BANNER]: boolean;
[ONYXKEYS.PERSISTED_REQUESTS]: OnyxTypes.Request[];
[ONYXKEYS.QUEUED_ONYX_UPDATES]: OnyxTypes.QueuedOnyxUpdates;
[ONYXKEYS.CURRENT_DATE]: string;
diff --git a/src/components/ConfirmContent.js b/src/components/ConfirmContent.js
index 6981fd451309..9a72d4e7d584 100644
--- a/src/components/ConfirmContent.js
+++ b/src/components/ConfirmContent.js
@@ -8,6 +8,8 @@ import Button from './Button';
import useLocalize from '../hooks/useLocalize';
import useNetwork from '../hooks/useNetwork';
import Text from './Text';
+import variables from '../styles/variables';
+import Icon from './Icon';
const propTypes = {
/** Title of the modal */
@@ -40,9 +42,30 @@ const propTypes = {
/** Whether we should show the cancel button */
shouldShowCancelButton: PropTypes.bool,
+ /** Icon to display above the title */
+ iconSource: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
+
+ /** Whether to center the icon / text content */
+ shouldCenterContent: PropTypes.bool,
+
+ /** Whether to stack the buttons */
+ shouldStackButtons: PropTypes.bool,
+
+ /** Styles for title */
+ // eslint-disable-next-line react/forbid-prop-types
+ titleStyles: PropTypes.arrayOf(PropTypes.object),
+
+ /** Styles for prompt */
+ // eslint-disable-next-line react/forbid-prop-types
+ promptStyles: PropTypes.arrayOf(PropTypes.object),
+
/** Styles for view */
// eslint-disable-next-line react/forbid-prop-types
contentStyles: PropTypes.arrayOf(PropTypes.object),
+
+ /** Styles for icon */
+ // eslint-disable-next-line react/forbid-prop-types
+ iconAdditionalStyles: PropTypes.arrayOf(PropTypes.object),
};
const defaultProps = {
@@ -55,36 +78,87 @@ const defaultProps = {
shouldDisableConfirmButtonWhenOffline: false,
shouldShowCancelButton: true,
contentStyles: [],
+ iconSource: null,
+ shouldCenterContent: false,
+ shouldStackButtons: true,
+ titleStyles: [],
+ promptStyles: [],
+ iconAdditionalStyles: [],
};
function ConfirmContent(props) {
const {translate} = useLocalize();
const {isOffline} = useNetwork();
+ const isCentered = props.shouldCenterContent;
+
return (
-
-
+
+ {!_.isEmpty(props.iconSource) ||
+ (_.isFunction(props.iconSource) && (
+
+
+
+ ))}
+
+
+
+
+
+ {_.isString(props.prompt) ? {props.prompt} : props.prompt}
- {_.isString(props.prompt) ? {props.prompt} : props.prompt}
-
-
- {props.shouldShowCancelButton && (
-
+ {props.shouldStackButtons ? (
+ <>
+
+ {props.shouldShowCancelButton && (
+
+ )}
+ >
+ ) : (
+
+ {props.shouldShowCancelButton && (
+
+ )}
+
+
)}
);
diff --git a/src/components/ConfirmModal.js b/src/components/ConfirmModal.js
index 56bcfe933aaf..705a05ec2058 100755
--- a/src/components/ConfirmModal.js
+++ b/src/components/ConfirmModal.js
@@ -45,6 +45,27 @@ const propTypes = {
/** Should we announce the Modal visibility changes? */
shouldSetModalVisibility: PropTypes.bool,
+ /** Icon to display above the title */
+ iconSource: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
+
+ /** Styles for title */
+ // eslint-disable-next-line react/forbid-prop-types
+ titleStyles: PropTypes.arrayOf(PropTypes.object),
+
+ /** Styles for prompt */
+ // eslint-disable-next-line react/forbid-prop-types
+ promptStyles: PropTypes.arrayOf(PropTypes.object),
+
+ /** Styles for icon */
+ // eslint-disable-next-line react/forbid-prop-types
+ iconAdditionalStyles: PropTypes.arrayOf(PropTypes.object),
+
+ /** Whether to center the icon / text content */
+ shouldCenterContent: PropTypes.bool,
+
+ /** Whether to stack the buttons */
+ shouldStackButtons: PropTypes.bool,
+
...windowDimensionsPropTypes,
};
@@ -59,7 +80,13 @@ const defaultProps = {
shouldShowCancelButton: true,
shouldSetModalVisibility: true,
title: '',
+ iconSource: null,
onModalHide: () => {},
+ titleStyles: [],
+ iconAdditionalStyles: [],
+ promptStyles: [],
+ shouldCenterContent: false,
+ shouldStackButtons: true,
};
function ConfirmModal(props) {
@@ -85,6 +112,12 @@ function ConfirmModal(props) {
danger={props.danger}
shouldDisableConfirmButtonWhenOffline={props.shouldDisableConfirmButtonWhenOffline}
shouldShowCancelButton={props.shouldShowCancelButton}
+ shouldCenterContent={props.shouldCenterContent}
+ iconSource={props.iconSource}
+ iconAdditionalStyles={props.iconAdditionalStyles}
+ titleStyles={props.titleStyles}
+ promptStyles={props.promptStyles}
+ shouldStackButtons={props.shouldStackButtons}
/>
);
diff --git a/src/components/DownloadAppModal.js b/src/components/DownloadAppModal.js
new file mode 100644
index 000000000000..1eeab1c72fd3
--- /dev/null
+++ b/src/components/DownloadAppModal.js
@@ -0,0 +1,74 @@
+import React, {useState} from 'react';
+import PropTypes from 'prop-types';
+import {withOnyx} from 'react-native-onyx';
+import ONYXKEYS from '../ONYXKEYS';
+import styles from '../styles/styles';
+import CONST from '../CONST';
+import AppIcon from '../../assets/images/expensify-app-icon.svg';
+import useLocalize from '../hooks/useLocalize';
+import * as Link from '../libs/actions/Link';
+import * as Browser from '../libs/Browser';
+import getOperatingSystem from '../libs/getOperatingSystem';
+import setShowDownloadAppModal from '../libs/actions/DownloadAppModal';
+import ConfirmModal from './ConfirmModal';
+
+const propTypes = {
+ /** ONYX PROP to hide banner for a user that has dismissed it */
+ // eslint-disable-next-line react/forbid-prop-types
+ showDownloadAppBanner: PropTypes.bool,
+};
+
+const defaultProps = {
+ showDownloadAppBanner: true,
+};
+
+function DownloadAppModal({showDownloadAppBanner}) {
+ const [shouldShowBanner, setshouldShowBanner] = useState(Browser.isMobile() && showDownloadAppBanner);
+
+ const {translate} = useLocalize();
+
+ const handleCloseBanner = () => {
+ setShowDownloadAppModal(false);
+ setshouldShowBanner(false);
+ };
+
+ let link = '';
+
+ if (getOperatingSystem() === CONST.OS.IOS) {
+ link = CONST.APP_DOWNLOAD_LINKS.IOS;
+ } else if (getOperatingSystem() === CONST.OS.ANDROID) {
+ link = CONST.APP_DOWNLOAD_LINKS.ANDROID;
+ }
+
+ const handleOpenAppStore = () => {
+ Link.openExternalLink(link, true);
+ };
+
+ return (
+
+ );
+}
+
+DownloadAppModal.displayName = 'DownloadAppModal';
+DownloadAppModal.propTypes = propTypes;
+DownloadAppModal.defaultProps = defaultProps;
+
+export default withOnyx({
+ showDownloadAppBanner: {
+ key: ONYXKEYS.SHOW_DOWNLOAD_APP_BANNER,
+ },
+})(DownloadAppModal);
diff --git a/src/languages/en.js b/src/languages/en.js
index 08a4a7c93211..84b0561b3c38 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -247,6 +247,11 @@ export default {
newFaceEnterMagicCode: ({login}) => `It's always great to see a new face around here! Please enter the magic code sent to ${login}. It should arrive within a minute or two.`,
welcomeEnterMagicCode: ({login}) => `Please enter the magic code sent to ${login}. It should arrive within a minute or two.`,
},
+ DownloadAppModal: {
+ downloadTheApp: 'Download the app',
+ keepTheConversationGoing: 'Keep the conversation going in New Expensify, download the app for an enhanced experience.',
+ noThanks: 'No thanks',
+ },
login: {
hero: {
header: 'Split bills, request payments, and chat with friends.',
diff --git a/src/languages/es.js b/src/languages/es.js
index 3b077bc96673..eb2a667bdb38 100644
--- a/src/languages/es.js
+++ b/src/languages/es.js
@@ -246,6 +246,11 @@ export default {
newFaceEnterMagicCode: ({login}) => `¡Siempre es genial ver una cara nueva por aquí! Por favor ingresa el código mágico enviado a ${login}. Debería llegar en un par de minutos.`,
welcomeEnterMagicCode: ({login}) => `Por favor, introduce el código mágico enviado a ${login}. Debería llegar en un par de minutos.`,
},
+ DownloadAppModal: {
+ downloadTheApp: 'Descarga la aplicación',
+ keepTheConversationGoing: 'Mantén la conversación en New Expensify, descarga la aplicación para una experiencia mejorada.',
+ noThanks: 'No, gracias',
+ },
login: {
hero: {
header: 'Divida las facturas, solicite pagos y chatee con sus amigos.',
diff --git a/src/libs/actions/DownloadAppModal.js b/src/libs/actions/DownloadAppModal.js
new file mode 100644
index 000000000000..5dc2d3fdca22
--- /dev/null
+++ b/src/libs/actions/DownloadAppModal.js
@@ -0,0 +1,11 @@
+import Onyx from 'react-native-onyx';
+import ONYXKEYS from '../../ONYXKEYS';
+
+/**
+ * @param {Boolean} shouldShowBanner
+ */
+function setShowDownloadAppModal(shouldShowBanner) {
+ Onyx.set(ONYXKEYS.SHOW_DOWNLOAD_APP_BANNER, shouldShowBanner);
+}
+
+export default setShowDownloadAppModal;
diff --git a/src/styles/styles.js b/src/styles/styles.js
index 45dbe39d55a1..663570fc4d66 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -230,6 +230,11 @@ const styles = {
color: themeColors.textSupporting,
},
+ appIconBorderRadius: {
+ overflow: 'hidden',
+ borderRadius: 12,
+ },
+
unitCol: {
margin: 0,
padding: 0,
diff --git a/src/styles/utilities/spacing.js b/src/styles/utilities/spacing.js
index e4254102df9b..47b523d89ac2 100644
--- a/src/styles/utilities/spacing.js
+++ b/src/styles/utilities/spacing.js
@@ -487,6 +487,22 @@ export default {
gap: 4,
},
+ gap2: {
+ gap: 8,
+ },
+
+ gap3: {
+ gap: 12,
+ },
+
+ gap4: {
+ gap: 16,
+ },
+
+ gap5: {
+ gap: 20,
+ },
+
gap7: {
gap: 28,
},
diff --git a/src/styles/variables.js b/src/styles/variables.js
index b62e9e3cba7c..40e29ca3cf6e 100644
--- a/src/styles/variables.js
+++ b/src/styles/variables.js
@@ -28,6 +28,7 @@ export default {
componentBorderRadiusLarge: 16,
componentBorderRadiusCard: 12,
componentBorderRadiusRounded: 24,
+ downloadAppModalAppIconSize: 48,
buttonBorderRadius: 100,
avatarSizeLargeBordered: 88,
avatarSizeLarge: 80,