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);