diff --git a/src/CONST.ts b/src/CONST.ts index e5c070676e49..53197d41b85e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -514,6 +514,7 @@ const CONST = { MAX_DATE: '9999-12-31', MIN_DATE: '0001-01-01', ORDINAL_DAY_OF_MONTH: 'do', + MONTH_DAY_YEAR_ORDINAL_FORMAT: 'MMMM do, yyyy', }, SMS: { DOMAIN: '@expensify.sms', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index bab16698bcf6..f48a5cae92f0 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -716,6 +716,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/profile/address', getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/profile/address` as const, backTo), }, + WORKSPACE_PROFILE_PLAN: { + route: 'settings/workspaces/:policyID/profile/plan', + getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/profile/plan` as const, backTo), + }, WORKSPACE_ACCOUNTING: { route: 'settings/workspaces/:policyID/accounting', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting` as const, @@ -979,9 +983,9 @@ const ROUTES = { getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/category/${encodeURIComponent(categoryName)}` as const, }, WORKSPACE_UPGRADE: { - route: 'settings/workspaces/:policyID/upgrade/:featureName', - getRoute: (policyID: string, featureName: string, backTo?: string) => - getUrlWithBackToParam(`settings/workspaces/${policyID}/upgrade/${encodeURIComponent(featureName)}` as const, backTo), + route: 'settings/workspaces/:policyID/upgrade/:featureName?', + getRoute: (policyID: string, featureName?: string, backTo?: string) => + getUrlWithBackToParam(`settings/workspaces/${policyID}/upgrade/${encodeURIComponent(featureName ?? '')}` as const, backTo), }, WORKSPACE_DOWNGRADE: { route: 'settings/workspaces/:policyID/downgrade/', @@ -1162,16 +1166,16 @@ const ROUTES = { }, WORKSPACE_REPORT_FIELDS_LIST_VALUES: { route: 'settings/workspaces/:policyID/reportFields/listValues/:reportFieldID?', - getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/listValues/${encodeURIComponent(reportFieldID ?? '')}` as const, + getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/listValues/${reportFieldID ? encodeURIComponent(reportFieldID) : ''}` as const, }, WORKSPACE_REPORT_FIELDS_ADD_VALUE: { route: 'settings/workspaces/:policyID/reportFields/addValue/:reportFieldID?', - getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/addValue/${encodeURIComponent(reportFieldID ?? '')}` as const, + getRoute: (policyID: string, reportFieldID?: string) => `settings/workspaces/${policyID}/reportFields/addValue/${reportFieldID ? encodeURIComponent(reportFieldID) : ''}` as const, }, WORKSPACE_REPORT_FIELDS_VALUE_SETTINGS: { route: 'settings/workspaces/:policyID/reportFields/:valueIndex/:reportFieldID?', getRoute: (policyID: string, valueIndex: number, reportFieldID?: string) => - `settings/workspaces/${policyID}/reportFields/${valueIndex}/${encodeURIComponent(reportFieldID ?? '')}` as const, + `settings/workspaces/${policyID}/reportFields/${valueIndex}/${reportFieldID ? encodeURIComponent(reportFieldID) : ''}` as const, }, WORKSPACE_REPORT_FIELDS_EDIT_VALUE: { route: 'settings/workspaces/:policyID/reportFields/new/:valueIndex/edit', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index b608e327a563..86c71e182a2b 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -505,6 +505,7 @@ const SCREENS = { TAG_GL_CODE: 'Tag_GL_Code', CURRENCY: 'Workspace_Profile_Currency', ADDRESS: 'Workspace_Profile_Address', + PLAN: 'Workspace_Profile_Plan_Type', WORKFLOWS: 'Workspace_Workflows', WORKFLOWS_PAYER: 'Workspace_Workflows_Payer', WORKFLOWS_APPROVALS_NEW: 'Workspace_Approvals_New', diff --git a/src/languages/en.ts b/src/languages/en.ts index 457f2063ffb4..13aa4b17e358 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -193,6 +193,7 @@ import type { WelcomeNoteParams, WelcomeToRoomParams, WeSentYouMagicSignInLinkParams, + WorkspaceLockedPlanTypeParams, WorkspaceMemberList, WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams, WorkspaceYouMayJoin, @@ -2612,6 +2613,7 @@ const translations = { return 'Member'; } }, + planType: 'Plan type', submitExpense: 'Submit expenses using your workspace chat below:', defaultCategory: 'Default category', }, @@ -4366,6 +4368,19 @@ const translations = { moreDetails: 'for more details.', gotIt: 'Got it, thanks', }, + commonFeatures: { + title: 'Upgrade to the Control plan', + note: 'Unlock our most powerful features, including:', + benefits: { + note: 'The Control plan starts at $9 per active member per month.', + learnMore: 'Learn more', + pricing: 'about our plans and pricing.', + benefit1: 'Advanced accounting connections (NetSuite, Sage Intacct, and more)', + benefit2: 'Smart expense rules', + benefit3: 'Multi-level approval workflows', + benefit4: 'Enhanced security controls', + }, + }, }, restrictedAction: { restricted: 'Restricted', @@ -4469,6 +4484,25 @@ const translations = { andEnableWorkflows: 'and enable workflows, then add approvals to unlock this feature.', }, }, + planTypePage: { + planTypes: { + team: { + label: 'Collect', + description: 'For teams looking to automate their processes.', + }, + corporate: { + label: 'Control', + description: 'For organizations with advanced requirements.', + }, + }, + description: "Choose a plan that's right for you. For a detailed list of features and pricing, check out our", + subscriptionLink: 'plan types and pricing help page', + lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ + one: `You've committed to 1 active member on the Control plan until your annual subscription ends on ${annualSubscriptionEndDate}. You can switch to pay-per-use subscription and downgrade to the Collect plan starting ${annualSubscriptionEndDate} by disabling auto-renew in`, + other: `You've committed to ${count} active members on the Control plan until your annual subscription ends on ${annualSubscriptionEndDate}. You can switch to pay-per-use subscription and downgrade to the Collect plan starting ${annualSubscriptionEndDate} by disabling auto-renew in`, + }), + subscriptions: 'Subscriptions', + }, }, getAssistancePage: { title: 'Get assistance', diff --git a/src/languages/es.ts b/src/languages/es.ts index ef9ec53705ce..20202b2e176f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -193,6 +193,7 @@ import type { WelcomeNoteParams, WelcomeToRoomParams, WeSentYouMagicSignInLinkParams, + WorkspaceLockedPlanTypeParams, WorkspaceMemberList, WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams, WorkspaceYouMayJoin, @@ -2636,6 +2637,7 @@ const translations = { return 'Miembro'; } }, + planType: 'Tipo de plan', submitExpense: 'Envíe los gastos utilizando el chat de su espacio de trabajo:', defaultCategory: 'Categoría predeterminada', }, @@ -4333,6 +4335,25 @@ const translations = { confirmText: 'Sí, exportar de nuevo', cancelText: 'Cancelar', }, + planTypePage: { + planTypes: { + team: { + label: 'Collect', + description: 'Para equipos que buscan automatizar sus procesos.', + }, + corporate: { + label: 'Recolectar', + description: 'Para organizaciones con requisitos avanzados.', + }, + }, + description: 'Elige el plan adecuado para ti. Para ver una lista detallada de funciones y precios, consulta nuestra', + subscriptionLink: 'página de ayuda sobre tipos de planes y precios', + lockedPlanDescription: ({count, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => ({ + one: `Tienes un compromiso anual de 1 miembro activo en el plan Control hasta el ${annualSubscriptionEndDate}. Puedes cambiar a una suscripción de pago por uso y desmejorar al plan Recopilar a partir del ${annualSubscriptionEndDate} desactivando la renovación automática en`, + other: `Tienes un compromiso anual de ${count} miembros activos en el plan Control hasta el ${annualSubscriptionEndDate}. Puedes cambiar a una suscripción de pago por uso y desmejorar al plan Recopilar a partir del ${annualSubscriptionEndDate} desactivando la renovación automática en`, + }), + subscriptions: 'Suscripciones', + }, upgrade: { reportFields: { title: 'Los campos', @@ -4414,6 +4435,19 @@ const translations = { moreDetails: 'para obtener más información.', gotIt: 'Entendido, gracias.', }, + commonFeatures: { + title: 'Mejorar al plan Controlar', + note: 'Desbloquea nuestras funciones más potentes, incluyendo:', + benefits: { + note: 'El plan Controlar comienza desde $9 por miembro activo al mes.', + learnMore: 'Más información', + pricing: 'sobre nuestros planes y precios.', + benefit1: 'Conexiones avanzadas de contabilidad (NetSuite, Sage Intacct y más)', + benefit2: 'Reglas inteligentes de gastos', + benefit3: 'Flujos de aprobación de varios niveles', + benefit4: 'Controles de seguridad mejorados', + }, + }, }, restrictedAction: { restricted: 'Restringido', diff --git a/src/languages/params.ts b/src/languages/params.ts index f7180762d744..59e1a061f7bf 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -569,6 +569,11 @@ type CurrencyCodeParams = { currencyCode: string; }; +type WorkspaceLockedPlanTypeParams = { + count: number; + annualSubscriptionEndDate: string; +}; + type CompanyNameParams = { companyName: string; }; @@ -789,6 +794,7 @@ export type { WorkspaceMemberList, ImportPerDiemRatesSuccessfullDescriptionParams, CurrencyCodeParams, + WorkspaceLockedPlanTypeParams, CompanyNameParams, CustomUnitRateParams, ChatWithAccountManagerParams, diff --git a/src/libs/API/parameters/OpenWorkspacePlanPage.ts b/src/libs/API/parameters/OpenWorkspacePlanPage.ts new file mode 100644 index 000000000000..6081d5b289e7 --- /dev/null +++ b/src/libs/API/parameters/OpenWorkspacePlanPage.ts @@ -0,0 +1,5 @@ +type OpenWorkspacePlanPageParams = { + policyID: string; +}; + +export default OpenWorkspacePlanPageParams; diff --git a/src/libs/API/parameters/UpgradeToCorporateParams.ts b/src/libs/API/parameters/UpgradeToCorporateParams.ts index ee9d1359c4dd..7b7ff3e0adcc 100644 --- a/src/libs/API/parameters/UpgradeToCorporateParams.ts +++ b/src/libs/API/parameters/UpgradeToCorporateParams.ts @@ -1,6 +1,6 @@ type UpgradeToCorporateParams = { policyID: string; - featureName: string; + featureName?: string; }; export default UpgradeToCorporateParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 31844b69e5d4..ca68f3fcd4b7 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -356,4 +356,5 @@ export type {default as JoinAccessiblePolicyParams} from './JoinAccessiblePolicy export type {default as ImportPerDiemRatesParams} from './ImportPerDiemRatesParams'; export type {default as ExportPerDiemCSVParams} from './ExportPerDiemCSVParams'; export type {default as DismissProductTrainingParams} from './DismissProductTraining'; +export type {default as OpenWorkspacePlanPageParams} from './OpenWorkspacePlanPage'; export type {default as ResetSMSDeliveryFailureParams} from './ResetSMSDeliveryFailureParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 87dec9525bc8..f63be7c72f45 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -958,6 +958,7 @@ const READ_COMMANDS = { START_ISSUE_NEW_CARD_FLOW: 'StartIssueNewCardFlow', OPEN_CARD_DETAILS_PAGE: 'OpenCardDetailsPage', GET_ASSIGNED_SUPPORT_DATA: 'GetAssignedSupportData', + OPEN_WORKSPACE_PLAN_PAGE: 'OpenWorkspacePlanPage', } as const; type ReadCommand = ValueOf; @@ -1021,6 +1022,7 @@ type ReadCommandParameters = { [READ_COMMANDS.START_ISSUE_NEW_CARD_FLOW]: Parameters.StartIssueNewCardFlowParams; [READ_COMMANDS.OPEN_CARD_DETAILS_PAGE]: Parameters.OpenCardDetailsPageParams; [READ_COMMANDS.GET_ASSIGNED_SUPPORT_DATA]: Parameters.GetAssignedSupportDataParams; + [READ_COMMANDS.OPEN_WORKSPACE_PLAN_PAGE]: Parameters.OpenWorkspacePlanPageParams; }; const SIDE_EFFECT_REQUEST_COMMANDS = { diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 132cbdf7de90..82e762cf033d 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -271,6 +271,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/WorkspaceProfileCurrencyPage').default, [SCREENS.WORKSPACE.CATEGORY_SETTINGS]: () => require('../../../../pages/workspace/categories/CategorySettingsPage').default, [SCREENS.WORKSPACE.ADDRESS]: () => require('../../../../pages/workspace/WorkspaceProfileAddressPage').default, + [SCREENS.WORKSPACE.PLAN]: () => require('../../../../pages/workspace/WorkspaceProfilePlanTypePage').default, [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: () => require('../../../../pages/workspace/categories/WorkspaceCategoriesSettingsPage').default, [SCREENS.WORKSPACE.CATEGORIES_IMPORT]: () => require('../../../../pages/workspace/categories/ImportCategoriesPage').default, [SCREENS.WORKSPACE.CATEGORIES_IMPORTED]: () => require('../../../../pages/workspace/categories/ImportedCategoriesPage').default, diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index f36f154819f5..4365c5e65e25 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -5,6 +5,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { [SCREENS.WORKSPACE.PROFILE]: [ SCREENS.WORKSPACE.NAME, SCREENS.WORKSPACE.ADDRESS, + SCREENS.WORKSPACE.PLAN, SCREENS.WORKSPACE.CURRENCY, SCREENS.WORKSPACE.DESCRIPTION, SCREENS.WORKSPACE.SHARE, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 3824ec6e3273..30b64a79dca2 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -365,6 +365,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.ADDRESS]: { path: ROUTES.WORKSPACE_PROFILE_ADDRESS.route, }, + [SCREENS.WORKSPACE.PLAN]: { + path: ROUTES.WORKSPACE_PROFILE_PLAN.route, + }, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_IMPORT]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_IMPORT.route}, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CHART_OF_ACCOUNTS]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CHART_OF_ACCOUNTS.route}, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CLASSES]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CLASSES.route}, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index e48c6d8682e2..27671fac401f 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -246,7 +246,7 @@ type SettingsNavigatorParamList = { }; [SCREENS.WORKSPACE.UPGRADE]: { policyID: string; - featureName: string; + featureName?: string; backTo?: Routes; categoryId?: string; }; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index f77f992ede37..bebd54698288 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -28,7 +28,7 @@ function canUseCategoryAndTagApprovers(betas: OnyxEntry): boolean { function canUseCombinedTrackSubmit(): boolean { // We don't need to show this to all betas since this will be used for developing a feature for A/B testing. const session = SessionUtils.getSession(); - return isAccountIDEven(session?.accountID ?? -1); + return isAccountIDEven(session?.accountID ?? CONST.DEFAULT_NUMBER_ID); } function canUsePerDiem(betas: OnyxEntry): boolean { @@ -47,6 +47,14 @@ function canUseLinkPreviews(): boolean { return false; } +/** + * Workspace downgrade is temporarily disabled + * API is being integrated in this GH issue https://github.com/Expensify/App/issues/51494 + */ +function canUseWorkspaceDowngrade() { + return false; +} + export default { canUseDefaultRooms, canUseLinkPreviews, @@ -55,5 +63,6 @@ export default { canUseCombinedTrackSubmit, canUseCategoryAndTagApprovers, canUsePerDiem, + canUseWorkspaceDowngrade, shouldShowProductTrainingElements, }; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 9bf68b2c5432..025d40a54660 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -197,7 +197,15 @@ function getPolicyBrickRoadIndicatorStatus(policy: OnyxEntry, isConnecti } function getPolicyRole(policy: OnyxInputOrEntry | SearchPolicy, currentUserLogin: string | undefined) { - return policy?.role ?? policy?.employeeList?.[currentUserLogin ?? '-1']?.role; + if (policy?.role) { + return policy.role; + } + + if (!currentUserLogin) { + return; + } + + return policy?.employeeList?.[currentUserLogin]?.role; } /** @@ -391,7 +399,7 @@ function isPaidGroupPolicy(policy: OnyxEntry | SearchPolicy): boolean { } function getOwnedPaidPolicies(policies: OnyxCollection | null, currentUserAccountID: number): Policy[] { - return Object.values(policies ?? {}).filter((policy): policy is Policy => isPolicyOwner(policy, currentUserAccountID ?? -1) && isPaidGroupPolicy(policy)); + return Object.values(policies ?? {}).filter((policy): policy is Policy => isPolicyOwner(policy, currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID) && isPaidGroupPolicy(policy)); } function isControlPolicy(policy: OnyxEntry): boolean { @@ -400,7 +408,7 @@ function isControlPolicy(policy: OnyxEntry): boolean { function isTaxTrackingEnabled(isPolicyExpenseChat: boolean, policy: OnyxEntry, isDistanceRequest: boolean): boolean { const distanceUnit = getDistanceRateCustomUnit(policy); - const customUnitID = distanceUnit?.customUnitID ?? 0; + const customUnitID = distanceUnit?.customUnitID ?? CONST.DEFAULT_NUMBER_ID; const isPolicyTaxTrackingEnabled = isPolicyExpenseChat && policy?.tax?.trackingEnabled; const isTaxEnabledForDistance = isPolicyTaxTrackingEnabled && policy?.customUnits?.[customUnitID]?.attributes?.taxEnabled; @@ -538,7 +546,7 @@ function getDefaultApprover(policy: OnyxEntry | SearchPolicy): string { * Returns the accountID to whom the given expenseReport submits reports to in the given Policy. */ function getSubmitToAccountID(policy: OnyxEntry | SearchPolicy, expenseReport: OnyxEntry): number { - const employeeAccountID = expenseReport?.ownerAccountID ?? -1; + const employeeAccountID = expenseReport?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID; const employeeLogin = getLoginsByAccountIDs([employeeAccountID]).at(0) ?? ''; const defaultApprover = getDefaultApprover(policy); @@ -557,8 +565,8 @@ function getSubmitToAccountID(policy: OnyxEntry | SearchPolicy, expenseR return getAccountIDsByLogins([categoryAppover]).at(0) ?? -1; } - if (!tagApprover && getTagApproverRule(policy ?? '-1', tag)?.approver) { - tagApprover = getTagApproverRule(policy ?? '-1', tag)?.approver; + if (!tagApprover && getTagApproverRule(policy, tag)?.approver) { + tagApprover = getTagApproverRule(policy, tag)?.approver; } } @@ -709,7 +717,10 @@ function settingsPendingAction(settings?: string[], pendingFields?: PendingField } const key = Object.keys(pendingFields).find((setting) => settings.includes(setting)); - return pendingFields[key ?? '-1']; + if (!key) { + return; + } + return pendingFields[key]; } function findSelectedVendorWithDefaultSelect(vendors: NetSuiteVendor[] | undefined, selectedVendorId: string | undefined) { @@ -1078,7 +1089,7 @@ function getWorkspaceAccountID(policyID: string) { if (!policy) { return 0; } - return policy.workspaceAccountID ?? 0; + return policy.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID; } function hasVBBA(policyID: string) { @@ -1121,6 +1132,17 @@ function getActivePolicy(): OnyxEntry { return getPolicy(activePolicyId); } +function getUserFriendlyWorkspaceType(workspaceType: ValueOf) { + switch (workspaceType) { + case CONST.POLICY.TYPE.CORPORATE: + return Localize.translateLocal('workspace.type.control'); + case CONST.POLICY.TYPE.TEAM: + return Localize.translateLocal('workspace.type.collect'); + default: + return Localize.translateLocal('workspace.type.free'); + } +} + function isPolicyAccessible(policy: OnyxEntry): boolean { return !isEmptyObject(policy) && (Object.keys(policy).length !== 1 || isEmptyObject(policy.errors)) && !!policy?.id; } @@ -1254,6 +1276,7 @@ export { getNetSuiteImportCustomFieldLabel, getAllPoliciesLength, getActivePolicy, + getUserFriendlyWorkspaceType, isPolicyAccessible, areAllGroupPoliciesExpenseChatDisabled, }; diff --git a/src/libs/actions/Policy/Plan.ts b/src/libs/actions/Policy/Plan.ts new file mode 100644 index 000000000000..a30e8bb67704 --- /dev/null +++ b/src/libs/actions/Policy/Plan.ts @@ -0,0 +1,52 @@ +import type {OnyxUpdate} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import * as API from '@libs/API'; +import type {OpenWorkspacePlanPageParams} from '@libs/API/parameters'; +import {READ_COMMANDS} from '@libs/API/types'; +import Log from '@libs/Log'; +import ONYXKEYS from '@src/ONYXKEYS'; + +function OpenWorkspacePlanPage(policyID: string) { + if (!policyID) { + Log.warn('OpenWorkspacePlanPage invalid params', {policyID}); + return; + } + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + isLoading: true, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + isLoading: false, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + isLoading: false, + }, + }, + ]; + + const params: OpenWorkspacePlanPageParams = { + policyID, + }; + + API.read(READ_COMMANDS.OPEN_WORKSPACE_PLAN_PAGE, params, {optimisticData, successData, failureData}); +} + +export default OpenWorkspacePlanPage; diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index f81539d1e921..f855ea477856 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -196,7 +196,7 @@ Onyx.connect({ key: ONYXKEYS.SESSION, callback: (val) => { sessionEmail = val?.email ?? ''; - sessionAccountID = val?.accountID ?? -1; + sessionAccountID = val?.accountID ?? CONST.DEFAULT_NUMBER_ID; }, }); @@ -258,8 +258,8 @@ function hasInvoicingDetails(policy: OnyxEntry): boolean { * Returns a primary invoice workspace for the user */ function getInvoicePrimaryWorkspace(currentUserLogin: string | undefined): Policy | undefined { - if (PolicyUtils.canSendInvoiceFromWorkspace(activePolicyID ?? '-1')) { - return allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID ?? '-1'}`]; + if (PolicyUtils.canSendInvoiceFromWorkspace(activePolicyID)) { + return allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`]; } const activeAdminWorkspaces = PolicyUtils.getActiveAdminWorkspaces(allPolicies, currentUserLogin); return activeAdminWorkspaces.find((policy) => PolicyUtils.canSendInvoiceFromWorkspace(policy.id)); @@ -1700,6 +1700,7 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName outputCurrency: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, address: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, description: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + type: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }, }, }, @@ -1772,6 +1773,7 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName outputCurrency: null, address: null, description: null, + type: null, }, }, }, @@ -1862,7 +1864,7 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName failureData.push({ onyxMethod: Onyx.METHOD.SET, key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, - value: activePolicyID ?? '', + value: activePolicyID, }); } @@ -2234,7 +2236,7 @@ function createWorkspaceFromIOUPayment(iouReport: OnyxEntry): WorkspaceF expenseCreatedReportActionID: workspaceChatCreatedReportActionID, } = ReportUtils.buildOptimisticWorkspaceChats(policyID, workspaceName); - if (!employeeAccountID) { + if (!employeeAccountID || !oldPersonalPolicyID) { return; } @@ -2512,17 +2514,18 @@ function createWorkspaceFromIOUPayment(iouReport: OnyxEntry): WorkspaceF const parentReportActionID = iouReport.parentReportActionID; const reportPreview = iouReport?.parentReportID && parentReportActionID ? parentReport?.[parentReportActionID] : undefined; - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldChatReportID}`, - value: {[reportPreview?.reportActionID ?? '-1']: null}, - }); - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldChatReportID}`, - value: {[reportPreview?.reportActionID ?? '-1']: reportPreview}, - }); - + if (reportPreview?.reportActionID) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldChatReportID}`, + value: {[reportPreview?.reportActionID]: null}, + }); + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldChatReportID}`, + value: {[reportPreview?.reportActionID]: reportPreview}, + }); + } // To optimistically remove the GBR from the DM we need to update the hasOutstandingChildRequest param to false optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -2559,14 +2562,16 @@ function createWorkspaceFromIOUPayment(iouReport: OnyxEntry): WorkspaceF }); } - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${memberData.workspaceChatReportID}`, - value: {[reportPreview?.reportActionID ?? '-1']: null}, - }); + if (reportPreview?.reportActionID) { + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${memberData.workspaceChatReportID}`, + value: {[reportPreview?.reportActionID]: null}, + }); + } // Create the MOVED report action and add it to the DM chat which indicates to the user where the report has been moved - const movedReportAction = ReportUtils.buildOptimisticMovedReportAction(oldPersonalPolicyID ?? '-1', policyID, memberData.workspaceChatReportID, iouReportID, workspaceName); + const movedReportAction = ReportUtils.buildOptimisticMovedReportAction(oldPersonalPolicyID, policyID, memberData.workspaceChatReportID, iouReportID, workspaceName); optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oldChatReportID}`, @@ -3385,7 +3390,7 @@ function setForeignCurrencyDefault(policyID: string, taxCode: string) { API.write(WRITE_COMMANDS.SET_POLICY_TAXES_FOREIGN_CURRENCY_DEFAULT, parameters, onyxData); } -function upgradeToCorporate(policyID: string, featureName: string) { +function upgradeToCorporate(policyID: string, featureName?: string) { const policy = getPolicy(policyID); const optimisticData: OnyxUpdate[] = [ { @@ -3437,7 +3442,7 @@ function upgradeToCorporate(policyID: string, featureName: string) { }, ]; - const parameters: UpgradeToCorporateParams = {policyID, featureName}; + const parameters: UpgradeToCorporateParams = {policyID, ...(featureName ? {featureName} : {})}; API.write(WRITE_COMMANDS.UPGRADE_TO_CORPORATE, parameters, {optimisticData, successData, failureData}); } diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index 4a4862152d53..845937ceaf75 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -47,7 +47,7 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, route}: Workspac const {shouldUseNarrowLayout} = useResponsiveLayout(); const illustrations = useThemeIllustrations(); const {activeWorkspaceID, setActiveWorkspaceID} = useActiveWorkspace(); - const {canUseSpotnanaTravel} = usePermissions(); + const {canUseSpotnanaTravel, canUseWorkspaceDowngrade} = usePermissions(); const [currencyList = {}] = useOnyx(ONYXKEYS.CURRENCY_LIST); const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.accountID}); @@ -59,7 +59,7 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, route}: Workspac const formattedCurrency = !isEmptyObject(policy) && !isEmptyObject(currencyList) ? `${outputCurrency} - ${currencySymbol}` : ''; // We need this to update translation for deleting a workspace when it has third party card feeds or expensify card assigned. - const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policy?.id ?? '-1'); + const workspaceAccountID = policy?.id ? PolicyUtils.getWorkspaceAccountID(policy.id) : CONST.DEFAULT_NUMBER_ID; const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`); const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`); const hasCardFeedOrExpensifyCard = @@ -72,12 +72,42 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, route}: Workspac ? `${street1?.trim()}, ${street2 ? `${street2.trim()}, ` : ''}${policy.address.city}, ${policy.address.state} ${policy.address.zipCode ?? ''}` : ''; - const onPressCurrency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_CURRENCY.getRoute(policy?.id ?? '-1')), [policy?.id]); - const onPressAddress = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_ADDRESS.getRoute(policy?.id ?? '-1')), [policy?.id]); - const onPressName = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_NAME.getRoute(policy?.id ?? '-1')), [policy?.id]); - const onPressDescription = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_DESCRIPTION.getRoute(policy?.id ?? '-1')), [policy?.id]); - const onPressShare = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_SHARE.getRoute(policy?.id ?? '-1')), [policy?.id]); - + const onPressCurrency = useCallback(() => { + if (!policy?.id) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_PROFILE_CURRENCY.getRoute(policy.id)); + }, [policy?.id]); + const onPressAddress = useCallback(() => { + if (!policy?.id) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_PROFILE_ADDRESS.getRoute(policy.id)); + }, [policy?.id]); + const onPressName = useCallback(() => { + if (!policy?.id) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_PROFILE_NAME.getRoute(policy.id)); + }, [policy?.id]); + const onPressDescription = useCallback(() => { + if (!policy?.id) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_PROFILE_DESCRIPTION.getRoute(policy.id)); + }, [policy?.id]); + const onPressShare = useCallback(() => { + if (!policy?.id) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_PROFILE_SHARE.getRoute(policy.id)); + }, [policy?.id]); + const onPressPlanType = useCallback(() => { + if (!policy?.id) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_PROFILE_PLAN.getRoute(policy.id)); + }, [policy?.id]); const policyName = policy?.name ?? ''; const policyDescription = // policy?.description can be an empty string @@ -134,11 +164,11 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, route}: Workspac return; } - Policy.deleteWorkspace(policy?.id, policyName); + Policy.deleteWorkspace(policy.id, policyName); setIsDeleteModalOpen(false); // If the workspace being deleted is the active workspace, switch to the "All Workspaces" view - if (activeWorkspaceID === policy?.id) { + if (activeWorkspaceID === policy.id) { setActiveWorkspaceID(undefined); Navigation.navigateWithSwitchPolicyID({policyID: undefined}); } @@ -169,7 +199,12 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, route}: Workspac resizeMode="cover" /> Navigation.navigate(ROUTES.WORKSPACE_AVATAR.getRoute(policy?.id ?? '-1'))} + onViewPhotoPress={() => { + if (!policy?.id) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_AVATAR.getRoute(policy.id)); + }} source={policy?.avatarURL ?? ''} avatarID={policy?.id} size={CONST.AVATAR_SIZE.XLARGE} @@ -186,12 +221,27 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, route}: Workspac ]} editIconStyle={styles.smallEditIconWorkspace} isUsingDefaultAvatar={!policy?.avatarURL ?? false} - onImageSelected={(file) => Policy.updateWorkspaceAvatar(policy?.id ?? '-1', file as File)} - onImageRemoved={() => Policy.deleteWorkspaceAvatar(policy?.id ?? '-1')} + onImageSelected={(file) => { + if (!policy?.id) { + return; + } + Policy.updateWorkspaceAvatar(policy.id, file as File); + }} + onImageRemoved={() => { + if (!policy?.id) { + return; + } + Policy.deleteWorkspaceAvatar(policy.id); + }} editorMaskImage={Expensicons.ImageCropSquareMask} pendingAction={policy?.pendingFields?.avatarURL} errors={policy?.errorFields?.avatarURL} - onErrorClose={() => Policy.clearAvatarErrors(policy?.id ?? '-1')} + onErrorClose={() => { + if (!policy?.id) { + return; + } + Policy.clearAvatarErrors(policy.id); + }} previewSource={UserUtils.getFullSizeAvatar(policy?.avatarURL ?? '')} headerTitle={translate('workspace.common.workspaceAvatar')} originalFileName={policy?.originalFileName} @@ -216,7 +266,12 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, route}: Workspac Policy.clearPolicyErrorField(policy?.id ?? '-1', CONST.POLICY.COLLECTION_KEYS.DESCRIPTION)} + onClose={() => { + if (!policy?.id) { + return; + } + Policy.clearPolicyErrorField(policy.id, CONST.POLICY.COLLECTION_KEYS.DESCRIPTION); + }} > Policy.clearPolicyErrorField(policy?.id ?? '-1', CONST.POLICY.COLLECTION_KEYS.GENERAL_SETTINGS)} + onClose={() => { + if (!policy?.id) { + return; + } + Policy.clearPolicyErrorField(policy.id, CONST.POLICY.COLLECTION_KEYS.GENERAL_SETTINGS); + }} errorRowStyles={[styles.mt2]} > @@ -267,6 +327,23 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, route}: Workspac )} + + {!!canUseWorkspaceDowngrade && !readOnly && !!policy?.type && ( + + + + + + )} {!readOnly && (