From eb0b87a9e46f6b921c4b31b1dcf38868a791736d Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 9 Dec 2024 18:40:46 +0530 Subject: [PATCH 1/7] Implement upgrade to control when booking travel --- src/CONST.ts | 8 ++++++++ src/languages/en.ts | 5 +++++ src/languages/es.ts | 6 ++++++ src/libs/TripReservationUtils.ts | 4 ++++ 4 files changed, 23 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index 44b2fd16f64c..313cb6c986df 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -6262,6 +6262,14 @@ const CONST = { description: 'workspace.upgrade.perDiem.description' as const, icon: 'PerDiem', }, + travel: { + id: 'travel' as const, + alias: 'travel', + name: 'Travel', + title: 'workspace.upgrade.travel.title' as const, + description: 'workspace.upgrade.travel.description' as const, + icon: 'Luggage', + }, }; }, REPORT_FIELD_TYPES: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 26f7875e53ee..d4f73ea74655 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4272,6 +4272,11 @@ const translations = { 'Per diem is a great way to keep your daily costs compliant and predictable whenever your employees travel. Enjoy features like custom rates, default categories, and more granular details like destinations and subrates.', onlyAvailableOnPlan: 'Per diem are only available on the Control plan, starting at ', }, + travel: { + title: 'Travel', + description: 'Expensify Travel is a new corporate travel booking and management platform that allows members to book accommodations, flights, transportation, and more.', + onlyAvailableOnPlan: 'Travel is only available on the Control plan, starting at ', + }, pricing: { collect: '$5 ', amount: '$9 ', diff --git a/src/languages/es.ts b/src/languages/es.ts index 2be618953135..efba54bc8551 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4320,6 +4320,12 @@ const translations = { 'Las dietas per diem (ej.: $100 por día para comidas) son una excelente forma de mantener los gastos diarios predecibles y ajustados a las políticas de la empresa, especialmente si tus empleados viajan por negocios. Disfruta de funciones como tasas personalizadas, categorías por defecto y detalles más específicos como destinos y subtasas.', onlyAvailableOnPlan: 'Las dietas per diem solo están disponibles en el plan Control, a partir de ', }, + travel: { + title: 'Viajes', + description: + 'Expensify Travel es una nueva plataforma corporativa de reserva y gestión de viajes que permite a los miembros reservar alojamientos, vuelos, transporte y mucho más.', + onlyAvailableOnPlan: 'Travel solo está disponible en el plan Controlar, a partir de ', + }, note: { upgradeWorkspace: 'Mejore su espacio de trabajo para acceder a esta función, o', learnMore: 'más información', diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index f2ce5113af81..5e4f5e4918ff 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -90,6 +90,10 @@ function bookATrip(translate: LocaleContextProps['translate'], setCtaErrorMessag return; } const policy = PolicyUtils.getPolicy(activePolicyID); + if (!PolicyUtils.isControlPolicy(policy)) { + Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(activePolicyID ?? '-1', CONST.UPGRADE_FEATURE_INTRO_MAPPING.travel.alias, Navigation.getActiveRoute())); + return; + } if (isEmptyObject(policy?.address)) { Navigation.navigate(ROUTES.WORKSPACE_PROFILE_ADDRESS.getRoute(activePolicyID ?? '-1', Navigation.getActiveRoute())); return; From 0fc91c00552138905d2e9e0f0036b60c7c0d45cd Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Fri, 24 Jan 2025 00:47:09 +0530 Subject: [PATCH 2/7] Place new logic for collect plan upgrade --- src/ROUTES.ts | 1 + src/SCREENS.ts | 1 + src/components/WorkspaceConfirmationForm.tsx | 213 ++++++++++++++++++ src/languages/en.ts | 5 +- src/languages/es.ts | 5 +- .../ModalStackNavigators/index.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 1 + src/libs/TripReservationUtils.ts | 6 +- src/pages/Travel/TravelUpgrade.tsx | 69 ++++++ .../workspace/WorkspaceConfirmationPage.tsx | 188 +--------------- .../workspace/upgrade/UpgradeConfirmation.tsx | 45 ++-- 11 files changed, 333 insertions(+), 202 deletions(-) create mode 100644 src/components/WorkspaceConfirmationForm.tsx create mode 100644 src/pages/Travel/TravelUpgrade.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 33c94e343568..ef82d16ba865 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1483,6 +1483,7 @@ const ROUTES = { }, TRAVEL_MY_TRIPS: 'travel', TRAVEL_TCS: 'travel/terms', + TRAVEL_UPGRADE: 'travel/upgrade', TRACK_TRAINING_MODAL: 'track-training', TRAVEL_TRIP_SUMMARY: { route: 'r/:reportID/trip/:transactionID', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 3d85cd907f2a..0d8708aa7dee 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -30,6 +30,7 @@ const SCREENS = { TCS: 'Travel_TCS', TRIP_SUMMARY: 'Travel_TripSummary', TRIP_DETAILS: 'Travel_TripDetails', + UPGRADE: 'Travel_Upgrade', }, SEARCH: { CENTRAL_PANE: 'Search_Central_Pane', diff --git a/src/components/WorkspaceConfirmationForm.tsx b/src/components/WorkspaceConfirmationForm.tsx new file mode 100644 index 000000000000..d73faa27f089 --- /dev/null +++ b/src/components/WorkspaceConfirmationForm.tsx @@ -0,0 +1,213 @@ +import React, {useCallback, useMemo, useState} from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {generateDefaultWorkspaceName, generatePolicyID} from '@libs/actions/Policy/Policy'; +import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; +import {addErrorMessage} from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; +import {isRequiredFulfilled} from '@libs/ValidationUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/WorkspaceConfirmationForm'; +import Avatar from './Avatar'; +import AvatarWithImagePicker from './AvatarWithImagePicker'; +import CurrencyPicker from './CurrencyPicker'; +import FormProvider from './Form/FormProvider'; +import InputWrapper from './Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from './Form/types'; +import HeaderWithBackButton from './HeaderWithBackButton'; +import * as Expensicons from './Icon/Expensicons'; +import ScreenWrapper from './ScreenWrapper'; +import ScrollView from './ScrollView'; +import Text from './Text'; +import TextInput from './TextInput'; + +function getFirstAlphaNumericCharacter(str = '') { + return str + .normalize('NFD') + .replace(/[^0-9a-z]/gi, '') + .toUpperCase()[0]; +} + +type WorkspaceConfirmationSubmitFunctionParams = { + name: string; + currency: string; + avatarFile: File | CustomRNImageManipulatorResult | undefined; + policyID: string; +}; + +type WorkspaceConfirmationFormProps = { + /** The email of the workspace owner + * @summary Approved Accountants and Guides can enter a flow where they make a workspace for other users, + * and those are passed as a search parameter when using transition links + */ + policyOwnerEmail?: string; + + /** Submit function */ + onSubmit: (params: WorkspaceConfirmationSubmitFunctionParams) => void; + + /** go back function */ + onBackButtonPress?: () => void; +}; + +function WorkspaceConfirmationForm({onSubmit, policyOwnerEmail = '', onBackButtonPress = () => Navigation.goBack()}: WorkspaceConfirmationFormProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {inputCallbackRef} = useAutoFocusInput(); + + const validate = useCallback( + (values: FormOnyxValues) => { + const errors: FormInputErrors = {}; + const name = values.name.trim(); + + if (!isRequiredFulfilled(name)) { + errors.name = translate('workspace.editor.nameIsRequiredError'); + } else if ([...name].length > CONST.TITLE_CHARACTER_LIMIT) { + // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 + // code units. + addErrorMessage(errors, 'name', translate('common.error.characterLimitExceedCounter', {length: [...name].length, limit: CONST.TITLE_CHARACTER_LIMIT})); + } + + if (!isRequiredFulfilled(values[INPUT_IDS.CURRENCY])) { + errors[INPUT_IDS.CURRENCY] = translate('common.error.fieldRequired'); + } + + return errors; + }, + [translate], + ); + + const policyID = useMemo(() => generatePolicyID(), []); + const [session] = useOnyx(ONYXKEYS.SESSION); + + const [allPersonalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); + + const defaultWorkspaceName = generateDefaultWorkspaceName(policyOwnerEmail); + const [workspaceNameFirstCharacter, setWorkspaceNameFirstCharacter] = useState(defaultWorkspaceName ?? ''); + + const userCurrency = allPersonalDetails?.[session?.accountID ?? CONST.DEFAULT_NUMBER_ID]?.localCurrencyCode ?? CONST.CURRENCY.USD; + + const [workspaceAvatar, setWorkspaceAvatar] = useState<{avatarUri: string | null; avatarFileName?: string | null; avatarFileType?: string | null}>({ + avatarUri: null, + avatarFileName: null, + avatarFileType: null, + }); + const [avatarFile, setAvatarFile] = useState(); + + const stashedLocalAvatarImage = workspaceAvatar?.avatarUri ?? undefined; + + const DefaultAvatar = useCallback( + () => ( + + ), + [workspaceAvatar?.avatarUri, workspaceNameFirstCharacter, styles.alignSelfCenter, styles.avatarXLarge, policyID], + ); + + return ( + + + + + {translate('workspace.emptyWorkspace.subtitle')} + + { + setAvatarFile(image); + setWorkspaceAvatar({avatarUri: image.uri ?? '', avatarFileName: image.name ?? '', avatarFileType: image.type}); + }} + onImageRemoved={() => { + setAvatarFile(undefined); + setWorkspaceAvatar({avatarUri: null, avatarFileName: null, avatarFileType: null}); + }} + size={CONST.AVATAR_SIZE.XLARGE} + avatarStyle={[styles.avatarXLarge, styles.alignSelfCenter]} + shouldDisableViewPhoto + editIcon={Expensicons.Camera} + editIconStyle={styles.smallEditIconAccount} + type={CONST.ICON_TYPE_WORKSPACE} + style={[styles.w100, styles.alignItemsCenter, styles.mv4, styles.mb6, styles.alignSelfCenter, styles.ph5]} + DefaultAvatar={DefaultAvatar} + editorMaskImage={Expensicons.ImageCropSquareMask} + /> + + onSubmit({ + name: val[INPUT_IDS.NAME], + currency: val[INPUT_IDS.CURRENCY], + avatarFile, + policyID, + }) + } + enabledWhenOffline + > + + { + if (getFirstAlphaNumericCharacter(str) === getFirstAlphaNumericCharacter(workspaceNameFirstCharacter)) { + return; + } + setWorkspaceNameFirstCharacter(str); + }} + ref={inputCallbackRef} + /> + + + + + + + + + ); +} + +WorkspaceConfirmationForm.displayName = 'WorkspaceConfirmationForm'; + +export default WorkspaceConfirmationForm; + +export type {WorkspaceConfirmationSubmitFunctionParams}; diff --git a/src/languages/en.ts b/src/languages/en.ts index 03045d65a345..32d750749d2e 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3837,7 +3837,7 @@ const translations = { }, emptyWorkspace: { title: 'Create a workspace', - subtitle: 'Create a workspace to track receipts, reimburse expenses, send invoices, and more — all at the speed of chat.', + subtitle: 'Create a workspace to track receipts, reimburse expenses, manage travel, send invoices, and more — all at the speed of chat.', createAWorkspaceCTA: 'Get Started', features: { trackAndCollect: 'Track and collect receipts', @@ -4400,7 +4400,7 @@ const translations = { travel: { title: 'Travel', description: 'Expensify Travel is a new corporate travel booking and management platform that allows members to book accommodations, flights, transportation, and more.', - onlyAvailableOnPlan: 'Travel is only available on the Control plan, starting at ', + onlyAvailableOnPlan: 'Travel is available on the Collect plan, starting at ', }, pricing: { perActiveMember: 'per active member per month.', @@ -4415,6 +4415,7 @@ const translations = { headline: `You've upgraded your workspace!`, successMessage: ({policyName}: ReportPolicyNameParams) => `You've successfully upgraded ${policyName} to the Control plan!`, categorizeMessage: `You've successfully upgraded to a workspace on the Collect plan. Now you can categorize your expenses!`, + travelMessage: `You've successfully upgraded to a workspace on the Collect plan. Now you can start booking and managing travel!`, viewSubscription: 'View your subscription', moreDetails: 'for more details.', gotIt: 'Got it, thanks', diff --git a/src/languages/es.ts b/src/languages/es.ts index 24f8edaa1261..13412167cc42 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3881,7 +3881,7 @@ const translations = { }, emptyWorkspace: { title: 'Crea un espacio de trabajo', - subtitle: 'Crea un espacio de trabajo para organizar recibos, reembolsar gastos, enviar facturas y mucho más, todo a la velocidad del chat.', + subtitle: 'Crea un espacio de trabajo para organizar recibos, reembolsar gastos, gestionar viajes, enviar facturas y mucho más, todo a la velocidad del chat.', createAWorkspaceCTA: 'Comenzar', features: { trackAndCollect: 'Organiza recibos', @@ -4467,7 +4467,7 @@ const translations = { title: 'Viajes', description: 'Expensify Travel es una nueva plataforma corporativa de reserva y gestión de viajes que permite a los miembros reservar alojamientos, vuelos, transporte y mucho más.', - onlyAvailableOnPlan: 'Travel solo está disponible en el plan Controlar, a partir de ', + onlyAvailableOnPlan: 'Los viajes están disponibles en el plan Recopilar, a partir de ', }, note: { upgradeWorkspace: 'Mejore su espacio de trabajo para acceder a esta función, o', @@ -4481,6 +4481,7 @@ const translations = { completed: { headline: 'Has mejorado tu espacio de trabajo.', categorizeMessage: `Has actualizado con éxito a un espacio de trabajo en el plan Recopilar. ¡Ahora puedes categorizar tus gastos!`, + travelMessage: 'Has mejorado con éxito a un espacio de trabajo en el plan Recopilar. ¡Ahora puedes comenzar a reservar y gestionar viajes!', successMessage: ({policyName}: ReportPolicyNameParams) => `Has actualizado con éxito ${policyName} al plan Controlar.`, viewSubscription: 'Ver su suscripción', moreDetails: 'para obtener más información.', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 0b31401d7e25..76969ddd85d6 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -112,6 +112,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator({ [SCREENS.TRAVEL.MY_TRIPS]: () => require('../../../../pages/Travel/MyTripsPage').default, [SCREENS.TRAVEL.TCS]: () => require('../../../../pages/Travel/TravelTerms').default, + [SCREENS.TRAVEL.UPGRADE]: () => require('../../../../pages/Travel/TravelUpgrade').default, [SCREENS.TRAVEL.TRIP_SUMMARY]: () => require('../../../../pages/Travel/TripSummaryPage').default, [SCREENS.TRAVEL.TRIP_DETAILS]: () => require('../../../../pages/Travel/TripDetailsPage').default, }); diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 10544aef9e8e..09634ac00849 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1380,6 +1380,7 @@ const config: LinkingOptions['config'] = { screens: { [SCREENS.TRAVEL.MY_TRIPS]: ROUTES.TRAVEL_MY_TRIPS, [SCREENS.TRAVEL.TCS]: ROUTES.TRAVEL_TCS, + [SCREENS.TRAVEL.UPGRADE]: ROUTES.TRAVEL_UPGRADE, [SCREENS.TRAVEL.TRIP_SUMMARY]: ROUTES.TRAVEL_TRIP_SUMMARY.route, [SCREENS.TRAVEL.TRIP_DETAILS]: { path: ROUTES.TRAVEL_TRIP_DETAILS.route, diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index b1dfab10b8a6..93e124f2d4f0 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -16,7 +16,7 @@ import type IconAsset from '@src/types/utils/IconAsset'; import {openTravelDotLink} from './actions/Link'; import Log from './Log'; import Navigation from './Navigation/Navigation'; -import {getPolicy, isControlPolicy} from './PolicyUtils'; +import {getPolicy, isPaidGroupPolicy} from './PolicyUtils'; let travelSettings: OnyxEntry; Onyx.connect({ @@ -101,8 +101,8 @@ function bookATrip(translate: LocaleContextProps['translate'], setCtaErrorMessag return; } const policy = getPolicy(activePolicyID); - if (!isControlPolicy(policy)) { - Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(activePolicyID ?? '-1', CONST.UPGRADE_FEATURE_INTRO_MAPPING.travel.alias, Navigation.getActiveRoute())); + if (!isPaidGroupPolicy(policy)) { + Navigation.navigate(ROUTES.TRAVEL_UPGRADE); return; } if (isEmptyObject(policy?.address)) { diff --git a/src/pages/Travel/TravelUpgrade.tsx b/src/pages/Travel/TravelUpgrade.tsx new file mode 100644 index 000000000000..fbb7d32939c6 --- /dev/null +++ b/src/pages/Travel/TravelUpgrade.tsx @@ -0,0 +1,69 @@ +import React, {useState} from 'react'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import type {WorkspaceConfirmationSubmitFunctionParams} from '@components/WorkspaceConfirmationForm'; +import WorkspaceConfirmationForm from '@components/WorkspaceConfirmationForm'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import UpgradeConfirmation from '@pages/workspace/upgrade/UpgradeConfirmation'; +import UpgradeIntro from '@pages/workspace/upgrade/UpgradeIntro'; +import CONST from '@src/CONST'; +import {createDraftWorkspace, createWorkspace} from '@src/libs/actions/Policy/Policy'; + +function TravelUpgrade() { + const styles = useThemeStyles(); + const feature = CONST.UPGRADE_FEATURE_INTRO_MAPPING.travel; + const {translate} = useLocalize(); + const {isOffline} = useNetwork(); + + const [isUpgraded, setIsUpgraded] = useState(false); + const [shouldShowConfirmation, setShouldShowConfirmation] = useState(false); + + const onSubmit = (params: WorkspaceConfirmationSubmitFunctionParams) => { + createDraftWorkspace('', false, params.name, params.policyID, params.currency, params.avatarFile as File); + setShouldShowConfirmation(false); + setIsUpgraded(true); + createWorkspace('', false, params.name, params.policyID, '', params.currency, params.avatarFile as File); + }; + + if (shouldShowConfirmation) { + return ; + } + + return ( + + { + Navigation.goBack(); + }} + /> + {!!isUpgraded && ( + Navigation.goBack()} + policyName="" + isTravelUpgrade + /> + )} + {!isUpgraded && ( + setShouldShowConfirmation(true)} + buttonDisabled={isOffline} + loading={false} + isCategorizing + /> + )} + + ); +} + +TravelUpgrade.displayName = 'TravelUpgrade'; + +export default TravelUpgrade; diff --git a/src/pages/workspace/WorkspaceConfirmationPage.tsx b/src/pages/workspace/WorkspaceConfirmationPage.tsx index d5acbe8630a1..bbe174e33915 100644 --- a/src/pages/workspace/WorkspaceConfirmationPage.tsx +++ b/src/pages/workspace/WorkspaceConfirmationPage.tsx @@ -1,192 +1,26 @@ -import React, {useCallback, useMemo, useState} from 'react'; -import {View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; -import Avatar from '@components/Avatar'; -import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; -import CurrencyPicker from '@components/CurrencyPicker'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; -import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import * as Expensicons from '@components/Icon/Expensicons'; -import ScreenWrapper from '@components/ScreenWrapper'; -import ScrollView from '@components/ScrollView'; -import Text from '@components/Text'; -import TextInput from '@components/TextInput'; -import useAutoFocusInput from '@hooks/useAutoFocusInput'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; +import React from 'react'; +import WorkspaceConfirmationForm from '@components/WorkspaceConfirmationForm'; +import type {WorkspaceConfirmationSubmitFunctionParams} from '@components/WorkspaceConfirmationForm'; import {createWorkspaceWithPolicyDraftAndNavigateToIt} from '@libs/actions/App'; -import {generateDefaultWorkspaceName, generatePolicyID} from '@libs/actions/Policy/Policy'; -import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; -import {addErrorMessage} from '@libs/ErrorUtils'; import getCurrentUrl from '@libs/Navigation/currentUrl'; -import Navigation from '@libs/Navigation/Navigation'; -import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; -import {isRequiredFulfilled} from '@libs/ValidationUtils'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import INPUT_IDS from '@src/types/form/WorkspaceConfirmationForm'; -import withPolicy from './withPolicy'; - -function getFirstAlphaNumericCharacter(str = '') { - return str - .normalize('NFD') - .replace(/[^0-9a-z]/gi, '') - .toUpperCase()[0]; -} function WorkspaceConfirmationPage() { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const {inputCallbackRef} = useAutoFocusInput(); - - const validate = useCallback( - (values: FormOnyxValues) => { - const errors: FormInputErrors = {}; - const name = values.name.trim(); - - if (!isRequiredFulfilled(name)) { - errors.name = translate('workspace.editor.nameIsRequiredError'); - } else if ([...name].length > CONST.TITLE_CHARACTER_LIMIT) { - // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 - // code units. - addErrorMessage(errors, 'name', translate('common.error.characterLimitExceedCounter', {length: [...name].length, limit: CONST.TITLE_CHARACTER_LIMIT})); - } - - if (!isRequiredFulfilled(values[INPUT_IDS.CURRENCY])) { - errors[INPUT_IDS.CURRENCY] = translate('common.error.fieldRequired'); - } - - return errors; - }, - [translate], - ); - + const onSubmit = (params: WorkspaceConfirmationSubmitFunctionParams) => { + createWorkspaceWithPolicyDraftAndNavigateToIt('', params.name, false, false, '', params.policyID, params.currency, params.avatarFile as File); + }; const currentUrl = getCurrentUrl(); - const policyID = useMemo(() => generatePolicyID(), []); - const [session] = useOnyx(ONYXKEYS.SESSION); // Approved Accountants and Guides can enter a flow where they make a workspace for other users, // and those are passed as a search parameter when using transition links const policyOwnerEmail = currentUrl ? new URL(currentUrl).searchParams.get('ownerEmail') ?? '' : ''; - const [allPersonalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); - - const defaultWorkspaceName = generateDefaultWorkspaceName(policyOwnerEmail); - const [workspaceNameFirstCharacter, setWorkspaceNameFirstCharacter] = useState(defaultWorkspaceName ?? ''); - - const userCurrency = allPersonalDetails?.[session?.accountID ?? CONST.DEFAULT_NUMBER_ID]?.localCurrencyCode ?? CONST.CURRENCY.USD; - - const [workspaceAvatar, setWorkspaceAvatar] = useState<{avatarUri: string | null; avatarFileName?: string | null; avatarFileType?: string | null}>({ - avatarUri: null, - avatarFileName: null, - avatarFileType: null, - }); - const [avatarFile, setAvatarFile] = useState(); - - const stashedLocalAvatarImage = workspaceAvatar?.avatarUri ?? undefined; - - const DefaultAvatar = useCallback( - () => ( - - ), - [workspaceAvatar?.avatarUri, workspaceNameFirstCharacter, styles.alignSelfCenter, styles.avatarXLarge, policyID], - ); - return ( - - Navigation.goBack()} - /> - - - {translate('workspace.emptyWorkspace.subtitle')} - - { - setAvatarFile(image); - setWorkspaceAvatar({avatarUri: image.uri ?? '', avatarFileName: image.name ?? '', avatarFileType: image.type}); - }} - onImageRemoved={() => { - setAvatarFile(undefined); - setWorkspaceAvatar({avatarUri: null, avatarFileName: null, avatarFileType: null}); - }} - size={CONST.AVATAR_SIZE.XLARGE} - avatarStyle={[styles.avatarXLarge, styles.alignSelfCenter]} - shouldDisableViewPhoto - editIcon={Expensicons.Camera} - editIconStyle={styles.smallEditIconAccount} - type={CONST.ICON_TYPE_WORKSPACE} - style={[styles.w100, styles.alignItemsCenter, styles.mv4, styles.mb6, styles.alignSelfCenter, styles.ph5]} - DefaultAvatar={DefaultAvatar} - editorMaskImage={Expensicons.ImageCropSquareMask} - /> - { - createWorkspaceWithPolicyDraftAndNavigateToIt('', val[INPUT_IDS.NAME], false, false, '', policyID, val[INPUT_IDS.CURRENCY], avatarFile as File); - }} - enabledWhenOffline - > - - { - if (getFirstAlphaNumericCharacter(str) === getFirstAlphaNumericCharacter(workspaceNameFirstCharacter)) { - return; - } - setWorkspaceNameFirstCharacter(str); - }} - ref={inputCallbackRef} - /> - - - - - - - - + ); } WorkspaceConfirmationPage.displayName = 'WorkspaceConfirmationPage'; -export default withPolicy(WorkspaceConfirmationPage); +export default WorkspaceConfirmationPage; diff --git a/src/pages/workspace/upgrade/UpgradeConfirmation.tsx b/src/pages/workspace/upgrade/UpgradeConfirmation.tsx index 9824b2919d65..8e93a62a73c7 100644 --- a/src/pages/workspace/upgrade/UpgradeConfirmation.tsx +++ b/src/pages/workspace/upgrade/UpgradeConfirmation.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import ConfirmationPage from '@components/ConfirmationPage'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; @@ -10,31 +10,40 @@ type Props = { policyName: string; onConfirmUpgrade: () => void; isCategorizing?: boolean; + isTravelUpgrade?: boolean; }; -function UpgradeConfirmation({policyName, onConfirmUpgrade, isCategorizing}: Props) { +function UpgradeConfirmation({policyName, onConfirmUpgrade, isCategorizing, isTravelUpgrade}: Props) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const description = useMemo(() => { + if (isCategorizing) { + return translate('workspace.upgrade.completed.categorizeMessage'); + } + + if (isTravelUpgrade) { + return translate('workspace.upgrade.completed.travelMessage'); + } + + return ( + <> + {translate('workspace.upgrade.completed.successMessage', {policyName})}{' '} + Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION)} + > + {translate('workspace.upgrade.completed.viewSubscription')} + {' '} + {translate('workspace.upgrade.completed.moreDetails')} + + ); + }, [isCategorizing, isTravelUpgrade, policyName, styles.link, translate]); + return ( - {translate('workspace.upgrade.completed.successMessage', {policyName})}{' '} - Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION)} - > - {translate('workspace.upgrade.completed.viewSubscription')} - {' '} - {translate('workspace.upgrade.completed.moreDetails')} - - ) - } + description={description} shouldShowButton onButtonPress={onConfirmUpgrade} buttonText={translate('workspace.upgrade.completed.gotIt')} From 605835a01de44f87da2e7f85b464be7b808596c9 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 28 Jan 2025 23:52:30 +0530 Subject: [PATCH 3/7] Introduce some animation --- src/components/WorkspaceConfirmationForm.tsx | 9 ++---- src/libs/actions/Travel.ts | 1 - src/pages/Travel/TravelUpgrade.tsx | 30 +++++++++++++++++-- .../workspace/WorkspaceConfirmationPage.tsx | 15 +++++++--- 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/src/components/WorkspaceConfirmationForm.tsx b/src/components/WorkspaceConfirmationForm.tsx index d73faa27f089..b74cc279abee 100644 --- a/src/components/WorkspaceConfirmationForm.tsx +++ b/src/components/WorkspaceConfirmationForm.tsx @@ -21,7 +21,6 @@ import InputWrapper from './Form/InputWrapper'; import type {FormInputErrors, FormOnyxValues} from './Form/types'; import HeaderWithBackButton from './HeaderWithBackButton'; import * as Expensicons from './Icon/Expensicons'; -import ScreenWrapper from './ScreenWrapper'; import ScrollView from './ScrollView'; import Text from './Text'; import TextInput from './TextInput'; @@ -118,11 +117,7 @@ function WorkspaceConfirmationForm({onSubmit, policyOwnerEmail = '', onBackButto ); return ( - + <> - + ); } diff --git a/src/libs/actions/Travel.ts b/src/libs/actions/Travel.ts index 30debb0d9cc6..9ce0e0639273 100644 --- a/src/libs/actions/Travel.ts +++ b/src/libs/actions/Travel.ts @@ -167,5 +167,4 @@ function bookATrip(translate: LocaleContextProps['translate'], setCtaErrorMessag } } -// eslint-disable-next-line import/prefer-default-export export {acceptSpotnanaTerms, handleProvisioningPermissionDeniedError, openTravelDotAfterProvisioning, provisionDomain, bookATrip}; diff --git a/src/pages/Travel/TravelUpgrade.tsx b/src/pages/Travel/TravelUpgrade.tsx index fbb7d32939c6..0376fbcf4eb3 100644 --- a/src/pages/Travel/TravelUpgrade.tsx +++ b/src/pages/Travel/TravelUpgrade.tsx @@ -1,5 +1,6 @@ import React, {useState} from 'react'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Modal from '@components/Modal'; import ScreenWrapper from '@components/ScreenWrapper'; import type {WorkspaceConfirmationSubmitFunctionParams} from '@components/WorkspaceConfirmationForm'; import WorkspaceConfirmationForm from '@components/WorkspaceConfirmationForm'; @@ -28,6 +29,10 @@ function TravelUpgrade() { createWorkspace('', false, params.name, params.policyID, '', params.currency, params.avatarFile as File); }; + const onClose = () => { + setShouldShowConfirmation(false); + }; + if (shouldShowConfirmation) { return ; } @@ -40,10 +45,29 @@ function TravelUpgrade() { > { - Navigation.goBack(); - }} + onBackButtonPress={() => Navigation.goBack()} /> + + + + + {!!isUpgraded && ( Navigation.goBack()} diff --git a/src/pages/workspace/WorkspaceConfirmationPage.tsx b/src/pages/workspace/WorkspaceConfirmationPage.tsx index bbe174e33915..ae3356ddb215 100644 --- a/src/pages/workspace/WorkspaceConfirmationPage.tsx +++ b/src/pages/workspace/WorkspaceConfirmationPage.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import ScreenWrapper from '@components/ScreenWrapper'; import WorkspaceConfirmationForm from '@components/WorkspaceConfirmationForm'; import type {WorkspaceConfirmationSubmitFunctionParams} from '@components/WorkspaceConfirmationForm'; import {createWorkspaceWithPolicyDraftAndNavigateToIt} from '@libs/actions/App'; @@ -14,10 +15,16 @@ function WorkspaceConfirmationPage() { const policyOwnerEmail = currentUrl ? new URL(currentUrl).searchParams.get('ownerEmail') ?? '' : ''; return ( - + + + ); } From 96d8c44dce21ed0bc9e79ab1e210c29b4eec7d7d Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 29 Jan 2025 00:19:48 +0530 Subject: [PATCH 4/7] Fix bug --- src/pages/Travel/TravelUpgrade.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pages/Travel/TravelUpgrade.tsx b/src/pages/Travel/TravelUpgrade.tsx index 0376fbcf4eb3..3aa89fa60542 100644 --- a/src/pages/Travel/TravelUpgrade.tsx +++ b/src/pages/Travel/TravelUpgrade.tsx @@ -33,10 +33,6 @@ function TravelUpgrade() { setShouldShowConfirmation(false); }; - if (shouldShowConfirmation) { - return ; - } - return ( Date: Sun, 2 Feb 2025 22:24:15 +0530 Subject: [PATCH 5/7] Changes according to suggestion --- src/pages/Travel/TravelUpgrade.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/Travel/TravelUpgrade.tsx b/src/pages/Travel/TravelUpgrade.tsx index 3aa89fa60542..cead2e1353c8 100644 --- a/src/pages/Travel/TravelUpgrade.tsx +++ b/src/pages/Travel/TravelUpgrade.tsx @@ -64,14 +64,13 @@ function TravelUpgrade() { /> - {!!isUpgraded && ( + {isUpgraded ? ( Navigation.goBack()} policyName="" isTravelUpgrade /> - )} - {!isUpgraded && ( + ) : ( setShouldShowConfirmation(true)} From ec77e388ee7915f0804dd612fc50c0750c1c52ec Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 5 Feb 2025 22:56:58 +0530 Subject: [PATCH 6/7] Fix button after merge --- src/components/BookTravelButton.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/BookTravelButton.tsx b/src/components/BookTravelButton.tsx index 1953acde8ad4..edffb8315808 100644 --- a/src/components/BookTravelButton.tsx +++ b/src/components/BookTravelButton.tsx @@ -9,7 +9,7 @@ import {openTravelDotLink} from '@libs/actions/Link'; import {cleanupTravelProvisioningSession} from '@libs/actions/Travel'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; -import {getAdminsPrivateEmailDomains} from '@libs/PolicyUtils'; +import {getAdminsPrivateEmailDomains, isPaidGroupPolicy} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -52,6 +52,11 @@ function BookTravelButton({text}: BookTravelButtonProps) { return; } + if (!isPaidGroupPolicy(policy)) { + Navigation.navigate(ROUTES.TRAVEL_UPGRADE); + return; + } + // Spotnana requires an address anytime an entity is created for a policy if (isEmptyObject(policy?.address)) { Navigation.navigate(ROUTES.WORKSPACE_PROFILE_ADDRESS.getRoute(policy?.id, Navigation.getActiveRoute())); From 52b8f8c16a9988ec837b9106f6e807902c99ade5 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Wed, 5 Feb 2025 22:58:22 +0530 Subject: [PATCH 7/7] Fix lint --- src/pages/Travel/TravelUpgrade.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Travel/TravelUpgrade.tsx b/src/pages/Travel/TravelUpgrade.tsx index cead2e1353c8..78b8f8d7e488 100644 --- a/src/pages/Travel/TravelUpgrade.tsx +++ b/src/pages/Travel/TravelUpgrade.tsx @@ -26,7 +26,7 @@ function TravelUpgrade() { createDraftWorkspace('', false, params.name, params.policyID, params.currency, params.avatarFile as File); setShouldShowConfirmation(false); setIsUpgraded(true); - createWorkspace('', false, params.name, params.policyID, '', params.currency, params.avatarFile as File); + createWorkspace('', false, params.name, params.policyID, undefined, params.currency, params.avatarFile as File); }; const onClose = () => {