diff --git a/assets/images/product-illustrations/receipts-stacked-on-pin.svg b/assets/images/product-illustrations/receipts-stacked-on-pin.svg new file mode 100644 index 000000000000..4a4e2bd2f2a7 --- /dev/null +++ b/assets/images/product-illustrations/receipts-stacked-on-pin.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__paperairplane.svg b/assets/images/simple-illustrations/simple-illustration__paperairplane.svg new file mode 100644 index 000000000000..152a5d10f21c --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__paperairplane.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/CONST.ts b/src/CONST.ts index 3b23e2a7314a..ddced06f4bac 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -821,6 +821,7 @@ const CONST = { }, BETAS: { ALL: 'all', + AUTO_SUBMIT: 'autoSubmit', DEFAULT_ROOMS: 'defaultRooms', P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests', SPOTNANA_TRAVEL: 'spotnanaTravel', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index de46ec5e7623..e5a3ab00fa5d 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -163,6 +163,14 @@ const ONYXKEYS = { /** This NVP contains the referral banners the user dismissed */ NVP_DISMISSED_REFERRAL_BANNERS: 'nvp_dismissedReferralBanners', + /** + * This NVP contains if user has ever seen the instant submit explanation modal and user intent to not show the instant submit explanation modal again + * undefined : user has never seen the modal + * false : user has seen the modal but has not chosen "do not show again" + * true : user has seen the modal and does not want to see it again + */ + NVP_DISMISSED_INSTANT_SUBMIT_EXPLANATION: 'nvp_dismissedInstantSubmitExplanation', + /** This NVP contains the training modals the user denied showing again */ NVP_HAS_SEEN_TRACK_TRAINING: 'nvp_hasSeenTrackTraining', @@ -1052,6 +1060,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_RECENT_ATTENDEES]: Attendee[]; [ONYXKEYS.NVP_TRY_FOCUS_MODE]: boolean; [ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION]: boolean; + [ONYXKEYS.NVP_DISMISSED_INSTANT_SUBMIT_EXPLANATION]: boolean; [ONYXKEYS.NVP_LAST_PAYMENT_METHOD]: OnyxTypes.LastPaymentMethod; [ONYXKEYS.NVP_LAST_LOCATION_PERMISSION_PROMPT]: string; [ONYXKEYS.LAST_EXPORT_METHOD]: OnyxTypes.LastExportMethod; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 4aaa53a0ef6e..ec889fbf57d4 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -2029,6 +2029,7 @@ const ROUTES = { getRoute: (bossEmail?: string) => `onboarding/test-drive${bossEmail ? `?bossEmail=${encodeURIComponent(bossEmail)}` : ''}` as const, }, TEST_DRIVE_DEMO_ROOT: 'onboarding/test-drive/demo', + AUTO_SUBMIT_MODAL_ROOT: '/auto-submit', WORKSPACE_CONFIRMATION: { route: 'workspace/confirmation', getRoute: (backTo?: string) => getUrlWithBackToParam(`workspace/confirmation`, backTo), diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 9d53fe813d5a..405e4f13b9a0 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -681,7 +681,6 @@ const SCREENS = { TEST_DRIVE_MODAL: { ROOT: 'TestDrive_Modal_Root', }, - TEST_DRIVE_DEMO: { ROOT: 'TestDrive_Demo_Root', }, @@ -697,6 +696,7 @@ const SCREENS = { DETAILS_ROOT: 'Details_Root', PROFILE_ROOT: 'Profile_Root', PROCESS_MONEY_REQUEST_HOLD_ROOT: 'ProcessMoneyRequestHold_Root', + AUTO_SUBMIT_ROOT: 'AutoSubmit_Modal_Root', CHANGE_POLICY_EDUCATIONAL_ROOT: 'ChangePolicyEducational_Root', REPORT_DESCRIPTION_ROOT: 'Report_Description_Root', REPORT_PARTICIPANTS: { diff --git a/src/components/AutoSubmitModal.tsx b/src/components/AutoSubmitModal.tsx new file mode 100644 index 000000000000..df8cf7bbc3fc --- /dev/null +++ b/src/components/AutoSubmitModal.tsx @@ -0,0 +1,88 @@ +import React, {useCallback} from 'react'; +import {InteractionManager, View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import colors from '@styles/theme/colors'; +import variables from '@styles/variables'; +import {dismissInstantSubmitExplanation} from '@userActions/User'; +import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import FeatureTrainingModal from './FeatureTrainingModal'; +import Icon from './Icon'; +import * as Illustrations from './Icon/Illustrations'; +import Text from './Text'; + +const menuSections = [ + { + icon: Illustrations.PaperAirplane, + titleTranslationKey: 'autoSubmitModal.submittedExpensesTitle', + descriptionTranslationKey: 'autoSubmitModal.submittedExpensesDescription', + }, + { + icon: Illustrations.Pencil, + titleTranslationKey: 'autoSubmitModal.pendingExpensesTitle', + descriptionTranslationKey: 'autoSubmitModal.pendingExpensesDescription', + }, +]; + +function AutoSubmitModal() { + const [dismissedInstantSubmitExplanation] = useOnyx(ONYXKEYS.NVP_DISMISSED_INSTANT_SUBMIT_EXPLANATION, {canBeMissing: true}); + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + + const onClose = useCallback((willShowAgain: boolean) => { + InteractionManager.runAfterInteractions(() => { + if (!willShowAgain) { + dismissInstantSubmitExplanation(true); + } else { + dismissInstantSubmitExplanation(false); + } + }); + }, []); + + return ( + + {menuSections.map((section) => ( + + + + {translate(section.titleTranslationKey as TranslationPaths)} + {translate(section.descriptionTranslationKey as TranslationPaths)} + + + ))} + + ); +} + +export default AutoSubmitModal; +AutoSubmitModal.displayName = 'AutoSubmitModal'; diff --git a/src/components/FeatureTrainingModal.tsx b/src/components/FeatureTrainingModal.tsx index 372b8c80f299..fa0e1aabadfd 100644 --- a/src/components/FeatureTrainingModal.tsx +++ b/src/components/FeatureTrainingModal.tsx @@ -75,7 +75,7 @@ type BaseFeatureTrainingModalProps = { confirmText: string; /** A callback to call when user confirms the tutorial */ - onConfirm?: () => void; + onConfirm?: (willShowAgain: boolean) => void; /** A callback to call when modal closes */ onClose?: () => void; @@ -339,8 +339,8 @@ function FeatureTrainingModal({ if (shouldCloseOnConfirm) { closeModal(); } - onConfirm?.(); - }, [shouldCloseOnConfirm, onConfirm, closeModal]); + onConfirm?.(willShowAgain); + }, [shouldCloseOnConfirm, onConfirm, closeModal, willShowAgain]); /** * Extracts values from the non-scraped attribute WEB_PROP_ATTR at build time diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index e3712e8cf23f..cc6b978f182b 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -56,6 +56,7 @@ import MushroomTopHat from '@assets/images/product-illustrations/mushroom-top-ha import PaymentHands from '@assets/images/product-illustrations/payment-hands.svg'; import ReceiptYellow from '@assets/images/product-illustrations/receipt--yellow.svg'; import ReceiptsSearchYellow from '@assets/images/product-illustrations/receipts-search--yellow.svg'; +import ReceiptsStackedOnPin from '@assets/images/product-illustrations/receipts-stacked-on-pin.svg'; import RocketBlue from '@assets/images/product-illustrations/rocket--blue.svg'; import RocketOrange from '@assets/images/product-illustrations/rocket--orange.svg'; import RocketDude from '@assets/images/product-illustrations/rocket-dude.svg'; @@ -122,6 +123,7 @@ import MoneyIntoWallet from '@assets/images/simple-illustrations/simple-illustra import MoneyWings from '@assets/images/simple-illustrations/simple-illustration__moneywings.svg'; import OpenSafe from '@assets/images/simple-illustrations/simple-illustration__opensafe.svg'; import PalmTree from '@assets/images/simple-illustrations/simple-illustration__palmtree.svg'; +import PaperAirplane from '@assets/images/simple-illustrations/simple-illustration__paperairplane.svg'; import Pencil from '@assets/images/simple-illustrations/simple-illustration__pencil.svg'; import PerDiem from '@assets/images/simple-illustrations/simple-illustration__perdiem.svg'; import PiggyBank from '@assets/images/simple-illustrations/simple-illustration__piggybank.svg'; @@ -318,4 +320,6 @@ export { ReportReceipt, PlaidCompanyCardDetailLarge, PlaidCompanyCardDetail, + ReceiptsStackedOnPin, + PaperAirplane, }; diff --git a/src/languages/en.ts b/src/languages/en.ts index 05f99434cfae..3e7e4870341b 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -6672,6 +6672,14 @@ const translations = { }, callScheduled: 'Call scheduled', }, + autoSubmitModal: { + title: 'All clear and submitted!', + description: 'All warnings and violations has been cleared so:', + submittedExpensesTitle: 'These expenses have been submitted', + submittedExpensesDescription: 'These expenses have been sent to your approver but can still be edited until they are approved.', + pendingExpensesTitle: 'Pending expenses have been moved', + pendingExpensesDescription: 'Any pending card expenses have been moved to a separate report until they post.', + }, testDrive: { quickAction: { takeATwoMinuteTestDrive: 'Take a 2-minute test drive', diff --git a/src/languages/es.ts b/src/languages/es.ts index 27e5f0874eed..fb508a0be323 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7198,6 +7198,14 @@ const translations = { }, callScheduled: 'Llamada programada', }, + autoSubmitModal: { + title: '¡Todo claro y enviado!', + description: 'Se han solucionado todas las advertencias e infracciones, así que:', + submittedExpensesTitle: 'Estos gastos han sido enviados', + submittedExpensesDescription: 'Estos gastos se han enviado a tu aprobador pero aún se pueden editar hasta que sean aprobados.', + pendingExpensesTitle: 'Los gastos pendientes se han movido', + pendingExpensesDescription: 'Todo gasto de tarjeta pendiente se ha movido a un informe separado hasta que sea contabilizado.', + }, testDrive: { quickAction: { takeATwoMinuteTestDrive: 'Haz una proba de 2 minutos', diff --git a/src/libs/Navigation/AppNavigator/Navigators/FeatureTrainingModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/FeatureTrainingModalNavigator.tsx index 2041f7b0e4a5..acfd68c962fc 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/FeatureTrainingModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/FeatureTrainingModalNavigator.tsx @@ -1,5 +1,6 @@ import React from 'react'; import {View} from 'react-native'; +import AutoSubmitModal from '@components/AutoSubmitModal'; import NoDropZone from '@components/DragAndDrop/NoDropZone'; import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator'; import Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation'; @@ -28,6 +29,10 @@ function FeatureTrainingModalNavigator() { name={SCREENS.CHANGE_POLICY_EDUCATIONAL_ROOT} component={ChangePolicyEducationalModal} /> + diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 41b34f11354c..85d7a2fdcd43 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -54,6 +54,7 @@ const config: LinkingOptions['config'] = { exact: true, }, [SCREENS.PROCESS_MONEY_REQUEST_HOLD_ROOT]: ROUTES.PROCESS_MONEY_REQUEST_HOLD.route, + [SCREENS.AUTO_SUBMIT_ROOT]: ROUTES.AUTO_SUBMIT_MODAL_ROOT, [SCREENS.CHANGE_POLICY_EDUCATIONAL_ROOT]: ROUTES.CHANGE_POLICY_EDUCATIONAL.route, }, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index e41895598c7a..e524ee2b2ff4 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1610,6 +1610,7 @@ type SignInNavigatorParamList = { type FeatureTrainingNavigatorParamList = { [SCREENS.FEATURE_TRAINING_ROOT]: undefined; [SCREENS.PROCESS_MONEY_REQUEST_HOLD_ROOT]: undefined; + [SCREENS.AUTO_SUBMIT_ROOT]: undefined; [SCREENS.CHANGE_POLICY_EDUCATIONAL_ROOT]: undefined; }; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 90b9fa6d835d..c9533a2e8f4e 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -18,6 +18,15 @@ function canUseLinkPreviews(): boolean { return false; } +/** + * Checks if the user can use the auto-submit feature + * @param betas - The user's beta flags + * @returns true if the user can use auto-submit, false otherwise + */ +function canUseAutoSubmit(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.AUTO_SUBMIT) || canUseAllBetas(betas); +} + function isBetaEnabled(beta: Beta, betas: OnyxEntry): boolean { // This beta has been released to everyone, but in case user does not have the NVP loaded, we need to return true here. // Will be removed in this issue https://github.com/Expensify/App/issues/63254 @@ -31,4 +40,5 @@ export default { canUseLinkPreviews, isBlockedFromSpotnanaTravel, isBetaEnabled, + canUseAutoSubmit, }; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 8182b9d5ee47..64328ca671a0 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -1369,6 +1369,14 @@ function dismissTrackTrainingModal() { }); } +/** + * Dismiss the Auto-Submit explanation modal + * @param shouldDismiss Whether the user selected "Don't show again" + */ +function dismissInstantSubmitExplanation(shouldDismiss: boolean) { + Onyx.merge(ONYXKEYS.NVP_DISMISSED_INSTANT_SUBMIT_EXPLANATION, shouldDismiss); +} + function requestRefund() { API.write(WRITE_COMMANDS.REQUEST_REFUND, null); } @@ -1428,6 +1436,7 @@ export { closeAccount, dismissReferralBanner, dismissTrackTrainingModal, + dismissInstantSubmitExplanation, resendValidateCode, requestContactMethodValidateCode, updateNewsletterSubscription,