diff --git a/.env.example b/.env.example
index 944da2aa9296..601813eeab98 100644
--- a/.env.example
+++ b/.env.example
@@ -26,5 +26,7 @@ EXPENSIFY_ACCOUNT_ID_QA=-1
EXPENSIFY_ACCOUNT_ID_QA_TRAVIS=-1
EXPENSIFY_ACCOUNT_ID_RECEIPTS=-1
EXPENSIFY_ACCOUNT_ID_REWARDS=-1
+EXPENSIFY_ACCOUNT_ID_SAASTR=-1
+EXPENSIFY_ACCOUNT_ID_SBE=-1
EXPENSIFY_ACCOUNT_ID_STUDENT_AMBASSADOR=-1
EXPENSIFY_ACCOUNT_ID_SVFG=-1
diff --git a/src/CONST.js b/src/CONST.js
index 51d5649814ee..4234ed8a5ab7 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -873,6 +873,8 @@ const CONST = {
QA: 'qa@expensify.com',
QA_TRAVIS: 'qa+travisreceipts@expensify.com',
RECEIPTS: 'receipts@expensify.com',
+ SAASTR: 'saastr@expensify.com',
+ SBE: 'sbe@expensify.com',
STUDENT_AMBASSADOR: 'studentambassadors@expensify.com',
SVFG: 'svfg@expensify.com',
},
@@ -892,6 +894,8 @@ const CONST = {
QA_TRAVIS: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_QA_TRAVIS', 8595733)),
RECEIPTS: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_RECEIPTS', -1)),
REWARDS: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_REWARDS', 11023767)), // rewards@expensify.com
+ SAASTR: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_SAASTR', 15252830)),
+ SBE: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_SBE', 15305309)),
STUDENT_AMBASSADOR: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_STUDENT_AMBASSADOR', 10476956)),
SVFG: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_SVFG', 2012843)),
},
@@ -1228,6 +1232,8 @@ const CONST = {
this.EMAIL.QA,
this.EMAIL.QA_TRAVIS,
this.EMAIL.RECEIPTS,
+ this.EMAIL.SAASTR,
+ this.EMAIL.SBE,
this.EMAIL.STUDENT_AMBASSADOR,
this.EMAIL.SVFG,
];
@@ -1248,6 +1254,8 @@ const CONST = {
this.ACCOUNT_ID.QA_TRAVIS,
this.ACCOUNT_ID.RECEIPTS,
this.ACCOUNT_ID.REWARDS,
+ this.ACCOUNT_ID.SAASTR,
+ this.ACCOUNT_ID.SBE,
this.ACCOUNT_ID.STUDENT_AMBASSADOR,
this.ACCOUNT_ID.SVFG,
];
@@ -2591,6 +2599,10 @@ const CONST = {
NAVIGATE: 'NAVIGATE',
},
},
+ DEMO_PAGES: {
+ SAASTR: 'SaaStrDemoSetup',
+ SBE: 'SbeDemoSetup',
+ },
};
export default CONST;
diff --git a/src/Expensify.js b/src/Expensify.js
index d5455ca9db71..a1c398d0bb51 100644
--- a/src/Expensify.js
+++ b/src/Expensify.js
@@ -30,6 +30,7 @@ import KeyboardShortcutsModal from './components/KeyboardShortcutsModal';
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 DeeplinkWrapper from './components/DeeplinkWrapper';
// This lib needs to be imported, but it has nothing to export since all it contains is an Onyx connection
@@ -165,10 +166,16 @@ function Expensify(props) {
appStateChangeListener.current = AppState.addEventListener('change', initializeClient);
// If the app is opened from a deep link, get the reportID (if exists) from the deep link and navigate to the chat report
- Linking.getInitialURL().then((url) => Report.openReportFromDeepLink(url, isAuthenticated));
+ Linking.getInitialURL().then((url) => {
+ DemoActions.runDemoByURL(url);
+ Report.openReportFromDeepLink(url, isAuthenticated);
+ });
// Open chat report from a deep link (only mobile native)
- Linking.addEventListener('url', (state) => Report.openReportFromDeepLink(state.url, isAuthenticated));
+ Linking.addEventListener('url', (state) => {
+ DemoActions.runDemoByURL(state.url);
+ Report.openReportFromDeepLink(state.url, isAuthenticated);
+ });
return () => {
if (!appStateChangeListener.current) {
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 3c0b3ee9a6d6..074a5e99e6b1 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -218,6 +218,9 @@ const ONYXKEYS = {
// The access token to be used with the Mapbox library
MAPBOX_ACCESS_TOKEN: 'mapboxAccessToken',
+ // Information on any active demos being run
+ DEMO_INFO: 'demoInfo',
+
/** Collection Keys */
COLLECTION: {
DOWNLOAD: 'download_',
diff --git a/src/ROUTES.js b/src/ROUTES.js
index 3f96d77d477e..c73002482171 100644
--- a/src/ROUTES.js
+++ b/src/ROUTES.js
@@ -25,7 +25,6 @@ export default {
return `bank-account/${stepToOpen}?policyID=${policyID}${backToParam}`;
},
HOME: '',
- SAASTR_HOME: 'saastr',
SETTINGS: 'settings',
SETTINGS_PROFILE: 'settings/profile',
SETTINGS_SHARE_CODE: 'settings/shareCode',
@@ -186,6 +185,10 @@ export default {
getWorkspaceTravelRoute: (policyID) => `workspace/${policyID}/travel`,
getWorkspaceMembersRoute: (policyID) => `workspace/${policyID}/members`,
+ // These are some on-off routes that will be removed once they're no longer needed (see GH issues for details)
+ SAASTR: 'saastr',
+ SBE: 'sbe',
+
/**
* @param {String} route
* @returns {Object}
diff --git a/src/languages/en.js b/src/languages/en.js
index d0c945fbc37d..c931d2b07861 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -250,7 +250,6 @@ export default {
hero: {
header: 'Split bills, request payments, and chat with friends.',
body: 'Welcome to the future of Expensify, your new go-to place for financial collaboration with friends and teammates alike.',
- demoHeadline: 'Welcome to SaaStr! Hop in to start networking now.',
},
},
thirdPartySignIn: {
@@ -1629,4 +1628,12 @@ export default {
stateSelectorModal: {
placeholderText: 'Search to see options',
},
+ demos: {
+ saastr: {
+ signInWelcome: 'Welcome to SaaStr! Hop in to start networking now.',
+ },
+ sbe: {
+ signInWelcome: 'Welcome to Small Business Expo! Get paid back for your ride.',
+ },
+ },
};
diff --git a/src/languages/es.js b/src/languages/es.js
index 7f7457f686b8..0e6cfb43a0a5 100644
--- a/src/languages/es.js
+++ b/src/languages/es.js
@@ -249,7 +249,6 @@ export default {
hero: {
header: 'Divida las facturas, solicite pagos y chatee con sus amigos.',
body: 'Bienvenido al futuro de Expensify, tu nuevo lugar de referencia para la colaboración financiera con amigos y compañeros de equipo por igual.',
- demoHeadline: '¡Bienvenido a SaaStr! Entra y empieza a establecer contactos.',
},
},
thirdPartySignIn: {
@@ -2116,4 +2115,12 @@ export default {
stateSelectorModal: {
placeholderText: 'Buscar para ver opciones',
},
+ demos: {
+ saastr: {
+ signInWelcome: '¡Bienvenido a SaaStr! Entra y empieza a establecer contactos.',
+ },
+ sbe: {
+ signInWelcome: '¡Bienvenido a Small Business Expo! Recupera el dinero de tu viaje.',
+ },
+ },
};
diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator.js b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator.js
index 64eadcbe06c3..f685497e477b 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator.js
+++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator.js
@@ -2,9 +2,11 @@ import React from 'react';
import {createStackNavigator} from '@react-navigation/stack';
import SCREENS from '../../../../SCREENS';
import ReportScreenWrapper from '../ReportScreenWrapper';
+import DemoSetupPage from '../../../../pages/DemoSetupPage';
import getCurrentUrl from '../../currentUrl';
import styles from '../../../../styles/styles';
import FreezeWrapper from '../../FreezeWrapper';
+import CONST from '../../../../CONST';
const Stack = createStackNavigator();
@@ -28,6 +30,22 @@ function CentralPaneNavigator() {
}}
component={ReportScreenWrapper}
/>
+
+
);
diff --git a/src/libs/Navigation/AppNavigator/PublicScreens.js b/src/libs/Navigation/AppNavigator/PublicScreens.js
index e78e3179d4ac..7a87530a2d9e 100644
--- a/src/libs/Navigation/AppNavigator/PublicScreens.js
+++ b/src/libs/Navigation/AppNavigator/PublicScreens.js
@@ -1,7 +1,6 @@
import React from 'react';
import {createStackNavigator} from '@react-navigation/stack';
import SignInPage from '../../../pages/signin/SignInPage';
-import DemoSetupPage from '../../../pages/signin/DemoSetupPage';
import ValidateLoginPage from '../../../pages/ValidateLoginPage';
import LogInWithShortLivedAuthTokenPage from '../../../pages/LogInWithShortLivedAuthTokenPage';
import SCREENS from '../../../SCREENS';
@@ -20,11 +19,6 @@ function PublicScreens() {
options={defaultScreenOptions}
component={SignInPage}
/>
-
policy.owner === policyOwner);
+ if (!policyWithOwner) {
+ return null;
+ }
+
+ const expenseChat = _.find(allReports, (report) => isPolicyExpenseChat(report) && report.policyID === policyWithOwner.id);
+ if (!expenseChat) {
+ return null;
+ }
+ return expenseChat.reportID;
+}
+
/*
* @param {Object|null} report
* @returns {Boolean}
@@ -3390,6 +3407,7 @@ export {
getReportOfflinePendingActionAndErrors,
isDM,
getPolicy,
+ getPolicyExpenseChatReportIDByOwner,
shouldDisableSettings,
shouldDisableRename,
hasSingleParticipant,
diff --git a/src/libs/actions/DemoActions.js b/src/libs/actions/DemoActions.js
new file mode 100644
index 000000000000..aa2b43824f91
--- /dev/null
+++ b/src/libs/actions/DemoActions.js
@@ -0,0 +1,92 @@
+import Onyx from 'react-native-onyx';
+import _ from 'underscore';
+import lodashGet from 'lodash/get';
+import CONST from '../../CONST';
+import * as API from '../API';
+import * as ReportUtils from '../ReportUtils';
+import Navigation from '../Navigation/Navigation';
+import ROUTES from '../../ROUTES';
+import ONYXKEYS from '../../ONYXKEYS';
+import * as Localize from '../Localize';
+
+/**
+ * @param {String} workspaceOwnerEmail email of the workspace owner
+ * @param {String} apiCommand
+ */
+function createDemoWorkspaceAndNavigate(workspaceOwnerEmail, apiCommand) {
+ // Try to navigate to existing demo workspace expense chat if it exists in Onyx
+ const demoWorkspaceChatReportID = ReportUtils.getPolicyExpenseChatReportIDByOwner(workspaceOwnerEmail);
+ if (demoWorkspaceChatReportID) {
+ // We must call goBack() to remove the demo route from nav history
+ Navigation.goBack();
+ Navigation.navigate(ROUTES.getReportRoute(demoWorkspaceChatReportID));
+ return;
+ }
+
+ // We use makeRequestWithSideEffects here because we need to get the workspace chat report ID to navigate to it after it's created
+ // eslint-disable-next-line rulesdir/no-api-side-effects-method
+ API.makeRequestWithSideEffects(apiCommand).then((response) => {
+ // Get report updates from Onyx response data
+ const reportUpdate = _.find(response.onyxData, ({key}) => key === ONYXKEYS.COLLECTION.REPORT);
+ if (!reportUpdate) {
+ return;
+ }
+
+ // Get the policy expense chat update
+ const policyExpenseChatReport = _.find(reportUpdate.value, ({chatType}) => chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT);
+ if (!policyExpenseChatReport) {
+ return;
+ }
+
+ // Navigate to the new policy expense chat report
+ // Note: We must call goBack() to remove the demo route from history
+ Navigation.goBack();
+ Navigation.navigate(ROUTES.getReportRoute(policyExpenseChatReport.reportID));
+ });
+}
+
+function runSbeDemo() {
+ createDemoWorkspaceAndNavigate(CONST.EMAIL.SBE, 'CreateSbeDemoWorkspace');
+}
+
+function runSaastrDemo() {
+ createDemoWorkspaceAndNavigate(CONST.EMAIL.SAASTR, 'CreateSaastrDemoWorkspace');
+}
+
+/**
+ * Runs code for specific demos, based on the provided URL
+ *
+ * @param {String} url - URL user is navigating to via deep link (or regular link in web)
+ */
+function runDemoByURL(url = '') {
+ const cleanUrl = (url || '').toLowerCase();
+
+ if (cleanUrl.endsWith(ROUTES.SAASTR)) {
+ Onyx.set(ONYXKEYS.DEMO_INFO, {
+ saastr: {
+ isBeginningDemo: true,
+ },
+ });
+ } else if (cleanUrl.endsWith(ROUTES.SBE)) {
+ Onyx.set(ONYXKEYS.DEMO_INFO, {
+ sbe: {
+ isBeginningDemo: true,
+ },
+ });
+ } else {
+ // No demo is being run, so clear out demo info
+ Onyx.set(ONYXKEYS.DEMO_INFO, null);
+ }
+}
+
+function getHeadlineKeyByDemoInfo(demoInfo = {}) {
+ if (lodashGet(demoInfo, 'saastr.isBeginningDemo')) {
+ return Localize.translateLocal('demos.saastr.signInWelcome');
+ }
+ if (lodashGet(demoInfo, 'sbe.isBeginningDemo')) {
+ return Localize.translateLocal('demos.sbe.signInWelcome');
+ }
+ return '';
+}
+
+export {runSaastrDemo, runSbeDemo, runDemoByURL, getHeadlineKeyByDemoInfo};
diff --git a/src/pages/DemoSetupPage.js b/src/pages/DemoSetupPage.js
new file mode 100644
index 000000000000..53739820142b
--- /dev/null
+++ b/src/pages/DemoSetupPage.js
@@ -0,0 +1,41 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {useFocusEffect} from '@react-navigation/native';
+import FullScreenLoadingIndicator from '../components/FullscreenLoadingIndicator';
+import CONST from '../CONST';
+import * as DemoActions from '../libs/actions/DemoActions';
+import Navigation from '../libs/Navigation/Navigation';
+
+const propTypes = {
+ /** Navigation route context info provided by react navigation */
+ route: PropTypes.shape({
+ /** The exact route name used to get to this screen */
+ name: PropTypes.string.isRequired,
+ }).isRequired,
+};
+
+/*
+ * This is a "utility page", that does this:
+ * - Looks at the current route
+ * - Determines if there's a demo command we need to call
+ * - If not, routes back to home
+ */
+function DemoSetupPage(props) {
+ useFocusEffect(() => {
+ // Depending on the route that the user hit to get here, run a specific demo flow
+ if (props.route.name === CONST.DEMO_PAGES.SAASTR) {
+ DemoActions.runSaastrDemo();
+ } else if (props.route.name === CONST.DEMO_PAGES.SBE) {
+ DemoActions.runSbeDemo();
+ } else {
+ Navigation.goBack();
+ }
+ });
+
+ return ;
+}
+
+DemoSetupPage.propTypes = propTypes;
+DemoSetupPage.displayName = 'DemoSetupPage';
+
+export default DemoSetupPage;
diff --git a/src/pages/signin/DemoSetupPage.js b/src/pages/signin/DemoSetupPage.js
deleted file mode 100644
index f457c3a5421a..000000000000
--- a/src/pages/signin/DemoSetupPage.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-import SignInPage from './SignInPage';
-import useLocalize from '../../hooks/useLocalize';
-
-function DemoSetupPage() {
- const {translate} = useLocalize();
- return ;
-}
-
-DemoSetupPage.displayName = 'DemoSetupPage';
-
-export default DemoSetupPage;
diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js
index 6d846d872828..f1a8cf3b910e 100644
--- a/src/pages/signin/SignInPage.js
+++ b/src/pages/signin/SignInPage.js
@@ -19,6 +19,7 @@ import * as StyleUtils from '../../styles/StyleUtils';
import useLocalize from '../../hooks/useLocalize';
import useWindowDimensions from '../../hooks/useWindowDimensions';
import Log from '../../libs/Log';
+import * as DemoActions from '../../libs/actions/DemoActions';
const propTypes = {
/** The details about the account that the user is signing in with */
@@ -49,15 +50,19 @@ const propTypes = {
/** Whether or not the sign in page is being rendered in the RHP modal */
isInModal: PropTypes.bool,
- /** Override the green headline copy */
- customHeadline: PropTypes.string,
+ /** Information about any currently running demos */
+ demoInfo: PropTypes.shape({
+ saastr: PropTypes.shape({
+ isBeginningDemo: PropTypes.bool,
+ }),
+ }),
};
const defaultProps = {
account: {},
credentials: {},
isInModal: false,
- customHeadline: '',
+ demoInfo: {},
};
/**
@@ -85,7 +90,7 @@ function getRenderOptions({hasLogin, hasValidateCode, hasAccount, isPrimaryLogin
};
}
-function SignInPage({credentials, account, isInModal, customHeadline}) {
+function SignInPage({credentials, account, isInModal, demoInfo}) {
const {translate, formatPhoneNumber} = useLocalize();
const {isSmallScreenWidth} = useWindowDimensions();
const shouldShowSmallScreen = isSmallScreenWidth || isInModal;
@@ -109,6 +114,7 @@ function SignInPage({credentials, account, isInModal, customHeadline}) {
let welcomeHeader = '';
let welcomeText = '';
+ const customHeadline = DemoActions.getHeadlineKeyByDemoInfo(demoInfo);
const headerText = customHeadline || translate('login.hero.header');
if (shouldShowLoginForm) {
welcomeHeader = isSmallScreenWidth ? headerText : translate('welcomeText.getStarted');
@@ -179,4 +185,5 @@ SignInPage.displayName = 'SignInPage';
export default withOnyx({
account: {key: ONYXKEYS.ACCOUNT},
credentials: {key: ONYXKEYS.CREDENTIALS},
+ demoInfo: {key: ONYXKEYS.DEMO_INFO},
})(SignInPage);